From 9ba4f9dfed1fb3d2689cbdefbd0a7547876f8469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anarthal=20=28Rub=C3=A9n=20P=C3=A9rez=29?= <34971811+anarthal@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:47:42 +0200 Subject: [PATCH] Pipeline API Removed sansio_algorithm base class close #75 --- .clang-format | 2 +- .drone.star | 2 +- doc/Jamfile | 4 + doc/qbk/00_main.qbk | 7 + doc/qbk/24_examples.qbk | 24 + doc/qbk/24_pipeline.qbk | 201 +++++ doc/qbk/helpers/PipelineRequestType.qbk | 15 + doc/qbk/helpers/PipelineStageType.qbk | 18 + doc/qbk/helpers/quickref.xml | 13 + doc/xsl/custom-overrides.xsl | 3 + example/CMakeLists.txt | 1 + example/Jamfile | 1 + example/patch_updates.cpp | 2 +- example/pipeline.cpp | 225 +++++ include/boost/mysql.hpp | 1 + include/boost/mysql/any_connection.hpp | 88 +- include/boost/mysql/detail/algo_params.hpp | 25 +- .../boost/mysql/detail/connection_impl.hpp | 41 +- include/boost/mysql/detail/engine_impl.hpp | 24 +- include/boost/mysql/detail/next_action.hpp | 57 +- include/boost/mysql/detail/pipeline.hpp | 165 ++++ .../boost/mysql/detail/pipeline_concepts.hpp | 56 ++ .../boost/mysql/detail/throw_on_error_loc.hpp | 2 +- .../mysql/detail/writable_field_traits.hpp | 13 + .../boost/mysql/error_with_diagnostics.hpp | 43 +- include/boost/mysql/impl/connection_impl.ipp | 1 + .../mysql/impl/error_with_diagnostics.ipp | 22 + .../connection_pool/connection_node.hpp | 23 +- .../connection_pool/connection_pool_impl.hpp | 4 +- .../internal/protocol/deserialization.hpp | 3 - .../impl/internal/protocol/serialization.hpp | 2 +- .../impl/internal/sansio/close_connection.hpp | 15 +- .../impl/internal/sansio/close_statement.hpp | 74 +- .../mysql/impl/internal/sansio/connect.hpp | 11 +- .../impl/internal/sansio/connection_state.hpp | 36 +- .../internal/sansio/connection_state_data.hpp | 41 +- .../mysql/impl/internal/sansio/execute.hpp | 93 +- .../mysql/impl/internal/sansio/handshake.hpp | 93 +- .../impl/internal/sansio/message_reader.hpp | 14 +- .../impl/internal/sansio/message_writer.hpp | 83 -- .../boost/mysql/impl/internal/sansio/ping.hpp | 47 +- .../internal/sansio/prepare_statement.hpp | 80 +- .../impl/internal/sansio/quit_connection.hpp | 18 +- .../internal/sansio/read_resultset_head.hpp | 39 +- .../impl/internal/sansio/read_some_rows.hpp | 40 +- .../sansio/read_some_rows_dynamic.hpp | 12 +- .../impl/internal/sansio/reset_connection.hpp | 106 +-- .../impl/internal/sansio/run_pipeline.hpp | 214 +++++ .../impl/internal/sansio/sansio_algorithm.hpp | 52 -- .../internal/sansio/set_character_set.hpp | 91 +- .../impl/internal/sansio/start_execution.hpp | 27 +- .../impl/internal/sansio/top_level_algo.hpp | 44 +- include/boost/mysql/impl/pipeline.hpp | 216 +++++ include/boost/mysql/impl/pipeline.ipp | 179 ++++ include/boost/mysql/pipeline.hpp | 796 ++++++++++++++++++ include/boost/mysql/src.hpp | 2 + test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/common/include/test_common/printing.hpp | 82 +- test/common/src/printing.cpp | 101 +++ test/integration/CMakeLists.txt | 2 + test/integration/Jamfile | 2 + .../snippets/get_any_connection.hpp | 53 ++ .../test/character_set_tracking.cpp | 1 + test/integration/test/pipeline.cpp | 228 +++++ test/integration/test/prepared_statements.cpp | 4 +- test/integration/test/snippets/pipeline.cpp | 308 +++++++ .../test/snippets/sql_formatting.cpp | 18 +- test/integration/test/spotchecks.cpp | 93 +- test/unit/CMakeLists.txt | 5 +- test/unit/Jamfile | 5 +- test/unit/include/test_unit/algo_test.hpp | 103 +-- .../create_prepare_statement_response.hpp | 77 ++ .../include/test_unit/create_query_frame.hpp | 11 +- test/unit/include/test_unit/fail_count.hpp | 2 +- test/unit/include/test_unit/pool_printing.hpp | 66 -- test/unit/include/test_unit/printing.hpp | 100 +-- test/unit/src/printing.cpp | 206 +++++ test/unit/test/connection_pool.cpp | 5 +- .../connection_pool/connection_pool_impl.cpp | 57 +- .../sansio_connection_node.cpp | 2 +- test/unit/test/detail/engine_impl.cpp | 6 +- .../detail/typing/readable_field_traits.cpp | 4 +- test/unit/test/field.cpp | 6 +- test/unit/test/field_view.cpp | 8 +- test/unit/test/metadata.cpp | 3 +- test/unit/test/pipeline.cpp | 767 +++++++++++++++++ test/unit/test/sansio/close_statement.cpp | 8 +- test/unit/test/sansio/execute.cpp | 234 +++-- test/unit/test/sansio/message_writer.cpp | 323 ------- test/unit/test/sansio/ping.cpp | 102 ++- test/unit/test/sansio/prepare_statement.cpp | 216 +++++ test/unit/test/sansio/read_resultset_head.cpp | 22 +- test/unit/test/sansio/read_some_rows.cpp | 64 +- .../test/sansio/read_some_rows_dynamic.cpp | 10 +- test/unit/test/sansio/reset_connection.cpp | 214 ++--- test/unit/test/sansio/run_pipeline.cpp | 603 +++++++++++++ test/unit/test/sansio/set_character_set.cpp | 135 ++- test/unit/test/sansio/start_execution.cpp | 2 +- test/unit/test/sansio/top_level_algo.cpp | 133 ++- test/unit/test/spotchecks/misc.cpp | 69 ++ 101 files changed, 6341 insertions(+), 1632 deletions(-) create mode 100644 doc/qbk/24_pipeline.qbk create mode 100644 doc/qbk/helpers/PipelineRequestType.qbk create mode 100644 doc/qbk/helpers/PipelineStageType.qbk create mode 100644 example/pipeline.cpp create mode 100644 include/boost/mysql/detail/pipeline.hpp create mode 100644 include/boost/mysql/detail/pipeline_concepts.hpp create mode 100644 include/boost/mysql/impl/error_with_diagnostics.ipp delete mode 100644 include/boost/mysql/impl/internal/sansio/message_writer.hpp create mode 100644 include/boost/mysql/impl/internal/sansio/run_pipeline.hpp delete mode 100644 include/boost/mysql/impl/internal/sansio/sansio_algorithm.hpp create mode 100644 include/boost/mysql/impl/pipeline.hpp create mode 100644 include/boost/mysql/impl/pipeline.ipp create mode 100644 include/boost/mysql/pipeline.hpp create mode 100644 test/common/src/printing.cpp create mode 100644 test/integration/include/test_integration/snippets/get_any_connection.hpp create mode 100644 test/integration/test/pipeline.cpp create mode 100644 test/integration/test/snippets/pipeline.cpp create mode 100644 test/unit/include/test_unit/create_prepare_statement_response.hpp delete mode 100644 test/unit/include/test_unit/pool_printing.hpp create mode 100644 test/unit/src/printing.cpp create mode 100644 test/unit/test/pipeline.cpp delete mode 100644 test/unit/test/sansio/message_writer.cpp create mode 100644 test/unit/test/sansio/prepare_statement.cpp create mode 100644 test/unit/test/sansio/run_pipeline.cpp diff --git a/.clang-format b/.clang-format index af17d663..3814faf7 100644 --- a/.clang-format +++ b/.clang-format @@ -99,7 +99,7 @@ ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH -IncludeIsMainRegex: "([-_](test|unittest))?$" +IncludeIsMainRegex: null IncludeIsMainSourceRegex: "" IndentGotoLabels: true IndentPPDirectives: None diff --git a/.drone.star b/.drone.star index ea1aff94..5713b564 100644 --- a/.drone.star +++ b/.drone.star @@ -291,9 +291,9 @@ def main(ctx): linux_b2('Linux B2 clang-14-arm64', _image('build-clang14'), toolset='clang-14', cxxstd='20', arch='arm64'), linux_b2('Linux B2 clang-16-sanit', _image('build-clang16'), toolset='clang-16', cxxstd='20', address_sanitizer=1, undefined_sanitizer=1), linux_b2('Linux B2 clang-16-i386-sanit', _image('build-clang16-i386'), toolset='clang-16', cxxstd='20', address_model=32, address_sanitizer=1, undefined_sanitizer=1), - linux_b2('Linux B2 gcc-5', _image('build-gcc5'), toolset='gcc-5', cxxstd='11'), # gcc-5 C++14 doesn't like my constexpr field_view linux_b2('Linux B2 clang-17', _image('build-clang17'), toolset='clang-17', cxxstd='20'), linux_b2('Linux B2 clang-18', _image('build-clang18'), toolset='clang-18', cxxstd='23'), + linux_b2('Linux B2 gcc-5', _image('build-gcc5'), toolset='gcc-5', cxxstd='11'), # gcc-5 C++14 doesn't like my constexpr field_view linux_b2('Linux B2 gcc-5-ts-executor', _image('build-gcc5'), toolset='gcc-5', cxxstd='11', use_ts_executor=1), linux_b2('Linux B2 gcc-6', _image('build-gcc6'), toolset='gcc-6', cxxstd='14,17'), linux_b2('Linux B2 gcc-10', _image('build-gcc10'), toolset='gcc-10', cxxstd='17,20'), diff --git a/doc/Jamfile b/doc/Jamfile index 1e0861d7..1d1243c4 100644 --- a/doc/Jamfile +++ b/doc/Jamfile @@ -64,8 +64,12 @@ docca.reference reference.qbk \"BOOST_MYSQL_OUTPUT_STRING=class\" \\ \"BOOST_MYSQL_FORMATTABLE=class\" \\ \"BOOST_MYSQL_STATIC_ROW=class\" \\ + \"BOOST_MYSQL_PIPELINE_REQUEST_TYPE=class\" \\ + \"BOOST_MYSQL_PIPELINE_STAGE_TYPE=class\" \\ + \"BOOST_MYSQL_WRITABLE_FIELD=class\" \\ \"BOOST_MYSQL_DECL=\" \\ \"BOOST_MYSQL_HAS_LOCAL_TIME=\" \\ + \"BOOST_NO_CXX17_DEDUCTION_GUIDES=\" \\ " SKIP_FUNCTION_MACROS=NO OUTPUT_LANGUAGE=English diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 2a72067e..acb881c7 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -42,10 +42,13 @@ [def __FieldViewFwdIterator__ [reflink2 FieldViewFwdIterator ['FieldViewFwdIterator]]] [def __Formattable__ [reflink2 Formattable ['Formattable]]] [def __OutputString__ [reflink2 OutputString ['OutputString]]] +[def __PipelineRequestType__ [reflink2 PipelineRequestType ['PipelineRequestType]]] +[def __PipelineStageType__ [reflink2 PipelineStageType ['PipelineStageType]]] [def __ResultsType__ [reflink2 ResultsType ['ResultsType]]] [def __SocketStream__ [reflink2 SocketStream ['SocketStream]]] [def __StaticRow__ [reflink2 StaticRow ['StaticRow]]] [def __Stream__ [reflink2 Stream ['Stream]]] +[def __WritableField__ [reflink2 WritableFieldTuple ['WritableField]]] [def __WritableFieldTuple__ [reflink2 WritableFieldTuple ['WritableFieldTuple]]] @@ -136,6 +139,7 @@ END [import ../../test/integration/test/snippets/any_connection.cpp] [import ../../test/integration/test/snippets/connection_pool.cpp] [import ../../test/integration/test/snippets/sql_formatting.cpp] +[import ../../test/integration/test/snippets/pipeline.cpp] [include 01_intro.qbk] [include 02_integrating.qbk] @@ -160,6 +164,7 @@ END [include 21_connection_pool.qbk] [include 22_sql_formatting.qbk] [include 23_sql_formatting_advanced.qbk] +[include 24_pipeline.qbk] [include 24_examples.qbk] [include 25_tests.qbk] @@ -173,6 +178,8 @@ END [include helpers/FieldViewFwdIterator.qbk] [include helpers/Formattable.qbk] [include helpers/OutputString.qbk] +[include helpers/PipelineRequestType.qbk] +[include helpers/PipelineStageType.qbk] [include helpers/ResultsType.qbk] [include helpers/SocketStream.qbk] [include helpers/StaticRow.qbk] diff --git a/doc/qbk/24_examples.qbk b/doc/qbk/24_examples.qbk index c0263a62..55cae636 100644 --- a/doc/qbk/24_examples.qbk +++ b/doc/qbk/24_examples.qbk @@ -31,6 +31,7 @@ Here is a list of available examples: # [link mysql.examples.batch_inserts_generic (Experimental) Generic batch inserts with Boost.Describe: extending format_sql] # [link mysql.examples.dynamic_filters (Experimental) Implements a query with several dynamic filters using client-side query formatting] # [link mysql.examples.patch_updates (Experimental) Implements a dynamic UPDATE query for PATCH-like update semantics using client-side query formatting] +# [link mysql.examples.pipeline (Experimental) Prepares, executes and closes statements in batch using the pipeline API] # [link mysql.examples.connection_pool (Experimental) A REST API server that uses connection pooling] # [@https://github.com/anarthal/servertech-chat The BoostServerTech chat project uses Boost.MySQL and Boost.Redis to implement a chat server] @@ -399,6 +400,29 @@ __assume_setup__ + + +[section:pipeline (Experimental) Pipelines] + +This example demonstrates how use [link mysql.pipeline the pipeline API] +to prepare, execute and close statements in batch. + +The example employs async functions with C++20 coroutines. + +__assume_setup__ + +[import ../../example/pipeline.cpp] +[example_pipeline] + +[endsect] + + + + + + + + [section:connection_pool (Experimental) Connection pools] This example demonstrates how to use [reflink connection_pool]. diff --git a/doc/qbk/24_pipeline.qbk b/doc/qbk/24_pipeline.qbk new file mode 100644 index 00000000..beaa5405 --- /dev/null +++ b/doc/qbk/24_pipeline.qbk @@ -0,0 +1,201 @@ +[/ + Copyright (c) 2019-2024 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) +] + +[section:pipeline (Experimental) Pipelines] +[nochunk] + +Functions like [refmemunq any_connection execute], [refmemunq any_connection prepare_statement] +and their async counterparts are half-duplex: +they write a single request to the server and wait for its response. +In contrast, pipelines can increase efficiency by coalescing several requests into a single message, saving round-trips to the server. + +[warning + The MySQL client/server protocol doesn't have explicit support for pipelines. [*From the server's point of view, + a pipeline is just a sequence of unrelated requests]. The server will try to execute all stages + in each pipeline, regardless of the result of previous stages. Pipelines are considered + an [*advanced feature]. Please read [link mysql.pipeline.pitfalls the pitfalls section] for more info. +] + +[note + This feature is experimental. Its API may change in subsequent releases. +] + +[heading Use cases] + +You should use pipelines for lightweight operations, dominated by round-trip time. Typical examples include: + +* Running connection setup code, involving operations like [refmemunq any_connection reset_connection], + [refmemunq any_connection set_character_set] or preparing statements. [reflink connection_pool] uses + pipelines to clean up connections for re-use. +* Preparing several statements, in batch. +* Executing and closing a statement in a single round-trip. + + +You should [*avoid] pipelines for the following cases: + +* When you can achieve the same functionality using semicolon-separated queries + (thus using [link mysql.multi_resultset.multi_queries multi-queries] and [link mysql.sql_formatting client-side SQL formatting]). + Multi-queries will stop after the first error, which is usually what you want. See [link mysql.pipeline.pitfalls this section] for more info. +* When running heavyweight queries, where the gains in round-trip time are not significant. +* When there are dependencies between stages in the pipeline. Lack of protocol support makes this use case impossible. + +If you're not sure, don't use this feature. + + + + + + +[heading Pipeline requests and responses] + +There are two interfaces to pipelines: dynamic ([reflink pipeline_request]) and static +([reflink static_pipeline_request]). Both are C++11 compatible. + +To run a dynamic pipeline, create a request object describing what should the pipeline do: + +[pipeline_dynamic_request] + +Use [refmem pipeline_request add] with any of the [link mysql.pipeline.reference available stage types] +to add stages your pipeline. + +To actually run the pipeline, create a response object and call +[refmem any_connection run_pipeline] or [refmemunq any_connection async_run_pipeline]: + +[pipeline_dynamic_run] + +Finally, you can access the statements using: + +[pipeline_dynamic_results] + + + + +[heading Static pipelines] + +If the type and number of stages in your pipeline is known at compile-time, you can use +static pipelines, instead. The mechanics are similar, except that some checks are moved +to compile-time. The pipeline in the previous example can be rewritten as: + +[pipeline_static] + + + + + + + + + +[heading:error Error handling] + +If any of the pipeline stages result in an error, the entire [refmemunq any_connection run_pipeline] operation +is considered failed. This means that [*if `run_pipipeline` completed successfully, all stages succeeded]. Recall that +[*all stages are always run, regardless of the outcome of previous stages]. + +If `run_pipipeline` fails, you can check which stages succeeded and failed by inspecting responses. +Response types like [refmem prepare_statement_stage response_type] are aliases for +[link mysql.error_handling.system_result `boost::system::result`], a vocabulary type that +can contain either a value or an error. For instance: + +[pipeline_errors] + +The module uses [reflink errcode_with_diagnostics] as the error type, +which contains an `error_code` and a [reflink diagnostics] object. + + + + +[heading:pitfalls Potential pitfalls] + +All requests in the pipeline are always run, regardless of the outcome of previous requests. As a result, some pipelines can behave non-intuitively: + +[pipeline_pitfalls_bad] + +Pipelines aren't the best fit here. Instead, you can express the same logic using semicolon-separated queries: + +[pipeline_pitfalls_good] + +Pipeline stages are run sequentially by the server. If any of the stages involves a heavyweight query, +the server won't process subsequent stages until the query completes. + + + +[heading:reference Pipeline stage reference] + +In the table below, the following variables are assumed: + +* `req` is a [reflink pipeline_request]. +* `stmt` is a valid [reflink statement]. +* `result` is a [reflink results] object. +* `conn` is an [reflink any_connection] object. + +[table:reference + [ + [Stage type] + [Example] + [When run, equivalent to...] + [Response type] + ] + [ + [ + [reflink execute_stage][br][br] + Behaves like [refmem any_connection execute] + ] + [[pipeline_reference_execute]] + [[pipeline_reference_execute_equivalent]] + [ + [reflink results] or [reflink errcode_with_diagnostics] + ] + ] + [ + [ + [reflink prepare_statement_stage][br][br] + Behaves like [refmem any_connection prepare_statement] + ] + [[pipeline_reference_prepare_statement]] + [[pipeline_reference_prepare_statement_equivalent]] + [ + [reflink statement] or [reflink errcode_with_diagnostics] + ] + ] + [ + [ + [reflink close_statement_stage][br][br] + Behaves like [refmem any_connection close_statement] + ] + [[pipeline_reference_close_statement]] + [[pipeline_reference_close_statement_equivalent]] + [ + Possibly empty [reflink errcode_with_diagnostics] + ] + ] + [ + [ + [reflink reset_connection_stage][br][br] + Behaves like [refmem any_connection reset_connection] + ] + [[pipeline_reference_reset_connection]] + [[pipeline_reference_reset_connection_equivalent]] + [ + Possibly empty [reflink errcode_with_diagnostics] + ] + ] + [ + [ + [reflink set_character_set_stage][br][br] + Behaves like [refmem any_connection set_character_set] + ] + [[pipeline_reference_set_character_set]] + [[pipeline_reference_set_character_set_equivalent]] + [ + Possibly empty [reflink errcode_with_diagnostics] + ] + ] +] + + +[endsect] \ No newline at end of file diff --git a/doc/qbk/helpers/PipelineRequestType.qbk b/doc/qbk/helpers/PipelineRequestType.qbk new file mode 100644 index 00000000..d70c33b1 --- /dev/null +++ b/doc/qbk/helpers/PipelineRequestType.qbk @@ -0,0 +1,15 @@ +[/ + Copyright (c) 2019-2024 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) +] + +[section:boost__mysql__PipelineRequestType PipelineRequestType concept] + +A type `T` satisfies `PipelineRequestType` if either: + +* It's exactly the [reflink pipeline_request] class. +* It's an instantiation of the [reflink static_pipeline_request] template class. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/helpers/PipelineStageType.qbk b/doc/qbk/helpers/PipelineStageType.qbk new file mode 100644 index 00000000..e5d86908 --- /dev/null +++ b/doc/qbk/helpers/PipelineStageType.qbk @@ -0,0 +1,18 @@ +[/ + Copyright (c) 2019-2024 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) +] + +[section:boost__mysql__PipelineStageType PipelineStageType concept] + +A type `T` satisfies `PipelineStageType` if it's exactly one of these types: + +* [reflink execute_stage] +* [reflink prepare_statement_stage] +* [reflink close_statement_stage] +* [reflink reset_connection_stage] +* [reflink set_character_set_stage] + +[endsect] \ No newline at end of file diff --git a/doc/qbk/helpers/quickref.xml b/doc/qbk/helpers/quickref.xml index 5e53c620..bbcb82e7 100644 --- a/doc/qbk/helpers/quickref.xml +++ b/doc/qbk/helpers/quickref.xml @@ -18,12 +18,14 @@ any_address any_connection any_connection_params + any_stage_response bad_field_access basic_format_context bound_statement_tuple bound_statement_iterator_range buffer_params character_set + close_statement_stage connect_params connection connection_pool @@ -31,7 +33,9 @@ date datetime diagnostics + errcode_with_diagnostics error_with_diagnostics + execute_stage execution_state field field_view @@ -45,9 +49,12 @@ metadata pfr_by_name pfr_by_position + pipeline_request pool_executor_params pool_params pooled_connection + prepare_statement_stage + reset_connection_stage results resultset_view resultset @@ -55,8 +62,10 @@ row_view rows rows_view + set_character_set_stage statement static_execution_state + static_pipeline_request static_results unix_path @@ -97,6 +106,7 @@ get_mysql_server_category get_mariadb_server_category make_error_code + make_pipeline_request runtime throw_on_error @@ -125,6 +135,8 @@ FieldViewFwdIterator Formattable OutputString + PipelineRequestType + PipelineStageType ResultsType SocketStream StaticRow @@ -138,6 +150,7 @@ ReadableField types WritableField types Formattable types + Pipeline stage reference String encoding diff --git a/doc/xsl/custom-overrides.xsl b/doc/xsl/custom-overrides.xsl index e20490b9..c8147198 100644 --- a/doc/xsl/custom-overrides.xsl +++ b/doc/xsl/custom-overrides.xsl @@ -24,10 +24,13 @@ 'FieldViewFwdIterator', 'Formattable', 'OutputString', + 'PipelineRequestType', + 'PipelineStageType', 'ResultsType', 'SocketStream', 'StaticRow', 'Stream', + 'WritableField', 'WritableFieldTuple' "/> diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index ecf63fc2..737cd324 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -77,6 +77,7 @@ set(REGULAR_EXAMPLES metadata ssl timeouts + pipeline ) foreach(FILE_NAME ${REGULAR_EXAMPLES}) diff --git a/example/Jamfile b/example/Jamfile index d1f9f022..48b041ea 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -58,6 +58,7 @@ run_regular_example async_futures ; run_regular_example metadata ; run_regular_example ssl ; run_regular_example timeouts ; +run_regular_example pipeline ; run_regular_example async_coroutines : /boost/mysql/test//boost_context_lib ; run_regular_example any_connection : /boost/mysql/test//boost_context_lib ; diff --git a/example/patch_updates.cpp b/example/patch_updates.cpp index 31fb7932..367bccbf 100644 --- a/example/patch_updates.cpp +++ b/example/patch_updates.cpp @@ -212,7 +212,7 @@ void main_impl(int argc, char** argv) params.username = args.username; params.password = args.password; params.database = "boost_mysql_examples"; - params.multi_queries = true; // TODO + params.multi_queries = true; // Connect to the server conn.connect(params); diff --git a/example/pipeline.cpp b/example/pipeline.cpp new file mode 100644 index 00000000..59aae740 --- /dev/null +++ b/example/pipeline.cpp @@ -0,0 +1,225 @@ +// +// Copyright (c) 2019-2024 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) +// + +//[example_pipeline + +// This example demonstrates how to use the pipeline API to prepare, +// execute and close statements in batch. +// It uses asynchronous functions and C++20 coroutines (with boost::asio::co_spawn). +// +// Pipelines are an experimental feature. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT + +namespace asio = boost::asio; + +using boost::mysql::error_code; +using boost::mysql::string_view; + +// Prepare several statements in batch. +// This is faster than preparing them one by one, as it saves round-trips to the server. +asio::awaitable> batch_prepare( + boost::mysql::any_connection& conn, + boost::span statements +) +{ + // Construct a pipeline request describing the work to be performed. + // There must be one prepare_statement_stage per statement to prepare + boost::mysql::pipeline_request req; + for (auto stmt_sql : statements) + req.add(boost::mysql::prepare_statement_stage(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. + // any_stage_response is a variant-like type that can hold the response of any stage type. + std::vector pipe_res; + boost::mysql::diagnostics diag; + auto [ec] = co_await conn.async_run_pipeline(req, pipe_res, diag, asio::as_tuple(asio::deferred)); + boost::mysql::throw_on_error(ec, diag); + + // If we got here, all statements were prepared successfully. + // pipe_res contains as many elements as statements.size(), holding statement objects + // Extract them into a vector + std::vector res; + res.reserve(statements.size()); + for (const auto& stage_res : pipe_res) + res.push_back(stage_res.get_statement()); + co_return res; +} + +void main_impl(int argc, char** argv) +{ + if (argc != 4 && argc != 5) + { + std::cerr << "Usage: " << argv[0] << " [company-id]\n"; + exit(1); + } + + const char* hostname = argv[3]; + + // The company_id to use when inserting new employees. + // This is user-supplied input, and should be treated as untrusted. + const char* company_id = argc == 5 ? argv[4] : "HGS"; + + // I/O context + boost::asio::io_context ctx; + + // Connection. Note that the connection's type doesn't depend + // on the transport (TCP or UNIX sockets). + boost::mysql::any_connection conn(ctx); + + // Connection configuration. This contains the server address, + // credentials, and other configuration used during connection establishment. + // Note that, by default, TCP connections will use TLS. connect_params::ssl + // allows disabling it. + boost::mysql::connect_params params; + + // The server address. This can either be a host and port or a UNIX socket path + params.server_address.emplace_host_and_port(hostname); + + // Username to log in as + params.username = argv[1]; + + // Password to use + params.password = argv[2]; + + // Database to use; leave empty or omit for no database + params.database = "boost_mysql_examples"; + + // Spawn a coroutine running the passed function + boost::asio::co_spawn( + ctx.get_executor(), + [&conn, ¶ms, company_id]() -> boost::asio::awaitable { + // Use as_tuple and throw_on_error to include diagnostics in our exceptions + constexpr auto tok = boost::asio::as_tuple(boost::asio::deferred); + boost::mysql::diagnostics diag; + + // Connect to the server + auto [ec] = co_await conn.async_connect(params, diag, tok); + boost::mysql::throw_on_error(ec, diag); + + // Prepare the statements using the batch prepare function that we previously defined + const std::array stmt_sql{ + "INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)", + "INSERT INTO audit_log (msg) VALUES (?)" + }; + std::vector stmts = co_await batch_prepare(conn, stmt_sql); + + // Create a request to execute them, this time using the static interface. + // Warning: do NOT include the COMMIT statement in this pipeline. + // COMMIT must only be executed if all the previous statements succeeded. + // In a pipeline, all stages get executed, regardless of the outcome of previous stages. + // We say that COMMIT has a dependency on the result of previous stages. + boost::mysql::static_pipeline_request req( + boost::mysql::execute_stage("START TRANSACTION"), + boost::mysql::execute_stage(stmts.at(0), {company_id, "Juan", "Lopez"}), + boost::mysql::execute_stage(stmts.at(0), {company_id, "Pepito", "Rodriguez"}), + boost::mysql::execute_stage(stmts.at(0), {company_id, "Someone", "Random"}), + boost::mysql::execute_stage(stmts.at(1), {"Inserted 3 new emplyees"}) + ); + decltype(req)::response_type res; + + // Execute the static pipeline + std::tie(ec) = co_await conn.async_run_pipeline(req, res, diag, tok); + boost::mysql::throw_on_error(ec, diag); + + // If we got here, all stages executed successfully. + // Since they were execution stages, the response contains a results object. + // Get the IDs of the newly created employees + auto id1 = std::get<1>(res)->last_insert_id(); + auto id2 = std::get<2>(res)->last_insert_id(); + auto id3 = std::get<3>(res)->last_insert_id(); + + // We can now commit our transaction and close the statements. + // Create the request to do so + boost::mysql::static_pipeline_request pipe2{ + boost::mysql::execute_stage("COMMIT"), + boost::mysql::close_statement_stage(stmts.at(0)), + boost::mysql::close_statement_stage(stmts.at(1)) + }; + decltype(pipe2)::response_type res2; + + // Run it + std::tie(ec) = co_await conn.async_run_pipeline(pipe2, res2, diag, tok); + boost::mysql::throw_on_error(ec, diag); + + // If we got here, our insertions got committed. + std::cout << "Inserted employees: " << id1 << ", " << id2 << ", " << id3 << std::endl; + + // Notify the MySQL server we want to quit, then close the underlying connection. + std::tie(ec) = co_await conn.async_close(diag, tok); + boost::mysql::throw_on_error(ec, diag); + }, + // If any exception is thrown in the coroutine body, rethrow it. + [](std::exception_ptr ptr) { + if (ptr) + { + std::rethrow_exception(ptr); + } + } + ); + + // Don't forget to call run()! Otherwise, your program + // will not spawn the coroutine and will do nothing. + ctx.run(); +} + +int main(int argc, char** argv) +{ + try + { + main_impl(argc, argv); + } + catch (const boost::mysql::error_with_diagnostics& err) + { + // You will only get this type of exceptions if you use throw_on_error. + // Some errors include additional diagnostics, like server-provided error messages. + // Security note: diagnostics::server_message may contain user-supplied values (e.g. the + // field value that caused the error) and is encoded using to the connection's character set + // (UTF-8 by default). Treat is as untrusted input. + std::cerr << "Error: " << err.what() << '\n' + << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; + return 1; + } + catch (const std::exception& err) + { + std::cerr << "Error: " << err.what() << std::endl; + return 1; + } +} + +#else + +int main(int, char**) { std::cout << "Sorry, your compiler does not support C++20 coroutines" << std::endl; } + +#endif + +//] diff --git a/include/boost/mysql.hpp b/include/boost/mysql.hpp index 262116f8..dfd31801 100644 --- a/include/boost/mysql.hpp +++ b/include/boost/mysql.hpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/mysql/any_connection.hpp b/include/boost/mysql/any_connection.hpp index f8adcbaa..5dd5daad 100644 --- a/include/boost/mysql/any_connection.hpp +++ b/include/boost/mysql/any_connection.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -129,6 +130,12 @@ class any_connection detail::any_address_view, decltype(asio::consign(std::declval(), std::unique_ptr()))>; + // Used by tests + any_connection(std::size_t initial_read_buffer_size, std::unique_ptr eng) + : impl_(initial_read_buffer_size, std::move(eng)) + { + } + public: /** * \brief Constructs a connection object from an executor and an optional set of parameters. @@ -140,7 +147,7 @@ public: * an \ref any_connection_params object to this constructor. */ any_connection(boost::asio::any_io_executor ex, any_connection_params params = {}) - : impl_(params.initial_read_buffer_size, create_engine(std::move(ex), params.ssl_context)) + : any_connection(params.initial_read_buffer_size, create_engine(std::move(ex), params.ssl_context)) { } @@ -1079,6 +1086,85 @@ public: std::forward(token) ); } + + /** + * \brief Runs a set of pipelined requests. + * \details + * Runs the pipeline described by `req` and stores its response in `res`. + * \n + * Request stages are seen by the server as a series of unrelated requests. + * As a consequence, all stages are always run, even if previous stages fail. + * + * If all stages succeed, the operation completes successfully. Thus, there is no need to check + * the per-stage error code in `res` if this operation completed successfully. + * \n + * If any stage fails with a non-fatal error (as per \ref is_fatal_error), the result of the operation + * is the first encountered error. You can check which stages succeeded and which ones didn't by + * inspecting each stage in `res`. + * \n + * If any stage fails with a fatal error, the result of the operation is the fatal error. + * Successive stages will be marked as failed with the fatal error. The server may or may + * not have processed such stages. + */ + template + void run_pipeline( + const PipelineRequestType& req, + typename PipelineRequestType::response_type& res, + error_code& err, + diagnostics& diag + ) + { + impl_.run(impl_.make_params_pipeline(req, res, diag), err); + } + + /// \copydoc run_pipeline + template + void run_pipeline(const PipelineRequestType& req, typename PipelineRequestType::response_type& res) + { + error_code err; + diagnostics diag; + run_pipeline(req, res, err, diag); + detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION); + } + + /** + * \copydoc run_pipeline + * \details + * \par Handler signature + * The handler signature for this operation is `void(boost::mysql::error_code)`. + * + * \par Object lifetimes + * The request and response objects must be kept alive and should not be modified + * until the operation completes. + */ + template < + BOOST_MYSQL_PIPELINE_REQUEST_TYPE PipelineRequestType, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code)) CompletionToken> + auto async_run_pipeline( + const PipelineRequestType& req, + typename PipelineRequestType::response_type& res, + CompletionToken&& token + ) BOOST_MYSQL_RETURN_TYPE(detail::async_run_pipeline_t) + { + return async_run_pipeline(req, res, impl_.shared_diag(), std::forward(token)); + } + + /// \copydoc async_run_pipeline + template < + BOOST_MYSQL_PIPELINE_REQUEST_TYPE PipelineRequestType, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code)) CompletionToken> + auto async_run_pipeline( + const PipelineRequestType& req, + typename PipelineRequestType::response_type& res, + diagnostics& diag, + CompletionToken&& token + ) BOOST_MYSQL_RETURN_TYPE(detail::async_run_pipeline_t) + { + return this->impl_.async_run( + impl_.make_params_pipeline(req, res, diag), + std::forward(token) + ); + } }; } // namespace mysql diff --git a/include/boost/mysql/detail/algo_params.hpp b/include/boost/mysql/detail/algo_params.hpp index 79b0dd58..035ab391 100644 --- a/include/boost/mysql/detail/algo_params.hpp +++ b/include/boost/mysql/detail/algo_params.hpp @@ -9,15 +9,14 @@ #define BOOST_MYSQL_DETAIL_ALGO_PARAMS_HPP #include -#include #include -#include -#include #include #include #include -#include +#include + +#include #include #include @@ -25,8 +24,15 @@ namespace boost { namespace mysql { +class rows_view; +class diagnostics; +class statement; + namespace detail { +class execution_processor; +class execution_state_impl; + struct connect_algo_params { diagnostics* diag; @@ -114,7 +120,6 @@ struct ping_algo_params struct reset_connection_algo_params { diagnostics* diag; - character_set charset; // set a non-empty character set to pipeline a SET NAMES with the reset request using result_type = void; }; @@ -141,8 +146,14 @@ struct close_connection_algo_params using result_type = void; }; -template -using has_void_result = std::is_same; +struct run_pipeline_algo_params +{ + diagnostics* diag; + detail::pipeline_request_view request; + detail::pipeline_response_ref response; + + using result_type = void; +}; } // namespace detail } // namespace mysql diff --git a/include/boost/mysql/detail/connection_impl.hpp b/include/boost/mysql/detail/connection_impl.hpp index 277255a6..75d44383 100644 --- a/include/boost/mysql/detail/connection_impl.hpp +++ b/include/boost/mysql/detail/connection_impl.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -135,6 +136,9 @@ make_request_getter(const bound_statement_tuple& req, std::v // helpers to run algos // +template +using has_void_result = std::is_same; + template struct completion_signature_impl; @@ -560,34 +564,24 @@ public: ping_algo_params make_params_ping(diagnostics& diag) const { return {&diag}; } // Reset connection - reset_connection_algo_params make_params_reset_connection( - diagnostics& diag, - const character_set& charset = {} - ) const - { - return {&diag, charset}; - } - - // Reset connection and issue a SET NAMES, using a pipeline. - // Used internally by connection_pool. - template - auto async_reset_with_charset(const character_set& charset, CompletionToken&& token) - BOOST_MYSQL_RETURN_TYPE(decltype(std::declval().async_run( - std::declval(), - std::forward(token) - ))) - { - return async_run( - make_params_reset_connection(shared_diag(), charset), - std::forward(token) - ); - } + reset_connection_algo_params make_params_reset_connection(diagnostics& diag) const { return {&diag}; } // Quit connection quit_connection_algo_params make_params_quit(diagnostics& diag) const { return {&diag}; } // Close connection close_connection_algo_params make_params_close(diagnostics& diag) const { return {&diag}; } + + // Run pipeline + template + static run_pipeline_algo_params make_params_pipeline( + const PipelineRequestType& req, + typename PipelineRequestType::response_type& response, + diagnostics& diag + ) + { + return {&diag, access::get_impl(req).to_view(), pipeline_response_ref(response)}; + } }; // To use some completion tokens, like deferred, in C++11, the old macros @@ -651,6 +645,9 @@ using async_quit_connection_t = async_run_t using async_close_connection_t = async_run_t; +template +using async_run_pipeline_t = async_run_t; + } // namespace detail } // namespace mysql } // namespace boost diff --git a/include/boost/mysql/detail/engine_impl.hpp b/include/boost/mysql/detail/engine_impl.hpp index ea50992d..67ee77f4 100644 --- a/include/boost/mysql/detail/engine_impl.hpp +++ b/include/boost/mysql/detail/engine_impl.hpp @@ -72,7 +72,7 @@ struct run_algo_op self.complete(stored_ec_); return; } - else if (act.type() == next_action::type_t::read) + else if (act.type() == next_action_type::read) { BOOST_MYSQL_YIELD( resume_point_, @@ -85,7 +85,7 @@ struct run_algo_op ) has_done_io_ = true; } - else if (act.type() == next_action::type_t::write) + else if (act.type() == next_action_type::write) { BOOST_MYSQL_YIELD( resume_point_, @@ -98,24 +98,24 @@ struct run_algo_op ) has_done_io_ = true; } - else if (act.type() == next_action::type_t::ssl_handshake) + else if (act.type() == next_action_type::ssl_handshake) { BOOST_MYSQL_YIELD(resume_point_, 4, stream_.async_ssl_handshake(std::move(self))) has_done_io_ = true; } - else if (act.type() == next_action::type_t::ssl_shutdown) + else if (act.type() == next_action_type::ssl_shutdown) { BOOST_MYSQL_YIELD(resume_point_, 5, stream_.async_ssl_shutdown(std::move(self))) has_done_io_ = true; } - else if (act.type() == next_action::type_t::connect) + else if (act.type() == next_action_type::connect) { BOOST_MYSQL_YIELD(resume_point_, 6, stream_.async_connect(std::move(self))) has_done_io_ = true; } else { - BOOST_ASSERT(act.type() == next_action::type_t::close); + BOOST_ASSERT(act.type() == next_action_type::close); stream_.close(io_ec); } } @@ -180,7 +180,7 @@ public: ec = act.error(); return; } - else if (act.type() == next_action::type_t::read) + else if (act.type() == next_action_type::read) { bytes_transferred = stream_.read_some( to_buffer(act.read_args().buffer), @@ -188,7 +188,7 @@ public: io_ec ); } - else if (act.type() == next_action::type_t::write) + else if (act.type() == next_action_type::write) { bytes_transferred = stream_.write_some( asio::buffer(act.write_args().buffer), @@ -196,21 +196,21 @@ public: io_ec ); } - else if (act.type() == next_action::type_t::ssl_handshake) + else if (act.type() == next_action_type::ssl_handshake) { stream_.ssl_handshake(io_ec); } - else if (act.type() == next_action::type_t::ssl_shutdown) + else if (act.type() == next_action_type::ssl_shutdown) { stream_.ssl_shutdown(io_ec); } - else if (act.type() == next_action::type_t::connect) + else if (act.type() == next_action_type::connect) { stream_.connect(io_ec); } else { - BOOST_ASSERT(act.type() == next_action::type_t::close); + BOOST_ASSERT(act.type() == next_action_type::close); stream_.close(io_ec); } } diff --git a/include/boost/mysql/detail/next_action.hpp b/include/boost/mysql/detail/next_action.hpp index 7c51feb9..23dcdc99 100644 --- a/include/boost/mysql/detail/next_action.hpp +++ b/include/boost/mysql/detail/next_action.hpp @@ -19,20 +19,20 @@ namespace boost { namespace mysql { namespace detail { +enum class next_action_type +{ + none, + write, + read, + ssl_handshake, + ssl_shutdown, + connect, + close, +}; + class next_action { public: - enum class type_t - { - none, - write, - read, - ssl_handshake, - ssl_shutdown, - connect, - close, - }; - struct read_args_t { span buffer; @@ -45,11 +45,11 @@ public: bool use_ssl; }; - next_action(error_code ec = {}) noexcept : type_(type_t::none), data_(ec) {} + next_action(error_code ec = {}) noexcept : type_(next_action_type::none), data_(ec) {} // Type - type_t type() const noexcept { return type_; } - bool is_done() const noexcept { return type_ == type_t::none; } + next_action_type type() const noexcept { return type_; } + bool is_done() const noexcept { return type_ == next_action_type::none; } bool success() const noexcept { return is_done() && !data_.ec; } // Arguments @@ -60,24 +60,33 @@ public: } read_args_t read_args() const noexcept { - BOOST_ASSERT(type_ == type_t::read); + BOOST_ASSERT(type_ == next_action_type::read); return data_.read_args; } write_args_t write_args() const noexcept { - BOOST_ASSERT(type_ == type_t::write); + BOOST_ASSERT(type_ == next_action_type::write); return data_.write_args; } - static next_action connect() noexcept { return next_action(type_t::connect, data_t()); } - static next_action read(read_args_t args) noexcept { return next_action(type_t::read, args); } - static next_action write(write_args_t args) noexcept { return next_action(type_t::write, args); } - static next_action ssl_handshake() noexcept { return next_action(type_t::ssl_handshake, data_t()); } - static next_action ssl_shutdown() noexcept { return next_action(type_t::ssl_shutdown, data_t()); } - static next_action close() noexcept { return next_action(type_t::close, data_t()); } + static next_action connect() noexcept { return next_action(next_action_type::connect, data_t()); } + static next_action read(read_args_t args) noexcept { return next_action(next_action_type::read, args); } + static next_action write(write_args_t args) noexcept + { + return next_action(next_action_type::write, args); + } + static next_action ssl_handshake() noexcept + { + return next_action(next_action_type::ssl_handshake, data_t()); + } + static next_action ssl_shutdown() noexcept + { + return next_action(next_action_type::ssl_shutdown, data_t()); + } + static next_action close() noexcept { return next_action(next_action_type::close, data_t()); } private: - type_t type_{type_t::none}; + next_action_type type_{next_action_type::none}; union data_t { error_code ec; @@ -90,7 +99,7 @@ private: data_t(write_args_t args) noexcept : write_args(args) {} } data_; - next_action(type_t t, data_t data) noexcept : type_(t), data_(data) {} + next_action(next_action_type t, data_t data) noexcept : type_(t), data_(data) {} }; } // namespace detail diff --git a/include/boost/mysql/detail/pipeline.hpp b/include/boost/mysql/detail/pipeline.hpp new file mode 100644 index 00000000..161f4386 --- /dev/null +++ b/include/boost/mysql/detail/pipeline.hpp @@ -0,0 +1,165 @@ +// +// Copyright (c) 2019-2024 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_DETAIL_PIPELINE_HPP +#define BOOST_MYSQL_DETAIL_PIPELINE_HPP + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +class execution_processor; + +enum class pipeline_stage_kind +{ + execute, + prepare_statement, + close_statement, + reset_connection, + set_character_set, + ping, +}; + +struct pipeline_request_stage +{ + pipeline_stage_kind kind; + std::uint8_t seqnum; + union stage_specific_t + { + std::nullptr_t nothing; + resultset_encoding enc; + character_set charset; + + stage_specific_t() noexcept : nothing() {} + stage_specific_t(resultset_encoding v) noexcept : enc(v) {} + stage_specific_t(character_set v) noexcept : charset(v) {} + } stage_specific; +}; + +struct pipeline_request_view +{ + span buffer; + span stages; +}; + +template +struct pipeline_response_traits; + +class pipeline_response_ref +{ + // We multiplex calls to store a single function pointer, instead of four + enum fn_type + { + fn_type_setup, + fn_type_get_processor, + fn_type_set_result, + fn_type_set_error + }; + + union fn_args + { + struct setup_t + { + fn_type type; + span request_stages; + + static_assert(std::is_standard_layout::value, "Internal error"); + } setup; + struct get_processor_t + { + fn_type type; + std::size_t index; + } get_processor; + struct set_result_t + { + fn_type type; + std::size_t index; + statement stmt; + } set_result; + struct set_error_t + { + fn_type type; + std::size_t index; + const error_code* ec; + diagnostics* diag; + } set_error; + + fn_args(span v) noexcept : setup{fn_type_setup, v} {} + fn_args(std::size_t index) noexcept : get_processor{fn_type_get_processor, index} {} + fn_args(std::size_t index, statement stmt) noexcept : set_result{fn_type_set_result, index, stmt} {} + fn_args(std::size_t index, const error_code* ec, diagnostics* diag) noexcept + : set_error{fn_type_set_error, index, ec, diag} + { + } + }; + + static execution_processor* null_invoke(void*, fn_args) { return nullptr; } + + template + static execution_processor* do_invoke(void* obj, fn_args args) + { + using traits_t = pipeline_response_traits; + + auto& self = *static_cast(obj); + switch (args.setup.type) + { + case fn_type_setup: traits_t::setup(self, args.setup.request_stages); return nullptr; + case fn_type_get_processor: return &traits_t::get_processor(self, args.get_processor.index); + case fn_type_set_result: + traits_t::set_result(self, args.set_result.index, args.set_result.stmt); + return nullptr; + case fn_type_set_error: + traits_t::set_error( + self, + args.set_error.index, + *args.set_error.ec, + std::move(*args.set_error.diag) + ); + return nullptr; + default: BOOST_ASSERT(false); return nullptr; + } + } + + void* obj_; + execution_processor* (*fn_)(void*, fn_args); + +public: + pipeline_response_ref() : obj_(nullptr), fn_(&null_invoke) {} + + template ::value>::type> + pipeline_response_ref(T& obj) : obj_(&obj), fn_(&do_invoke) + { + } + + void setup(span request_stages) { fn_(obj_, {request_stages}); } + execution_processor& get_processor(std::size_t stage_idx) { return *fn_(obj_, {stage_idx}); } + void set_result(std::size_t stage_idx, statement result) { fn_(obj_, {stage_idx, result}); } + void set_error(std::size_t stage_idx, error_code ec, diagnostics&& diag) + { + fn_(obj_, {stage_idx, &ec, &diag}); + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/detail/pipeline_concepts.hpp b/include/boost/mysql/detail/pipeline_concepts.hpp new file mode 100644 index 00000000..f840a022 --- /dev/null +++ b/include/boost/mysql/detail/pipeline_concepts.hpp @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2024 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_DETAIL_PIPELINE_CONCEPTS_HPP +#define BOOST_MYSQL_DETAIL_PIPELINE_CONCEPTS_HPP + +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +// PipelineStageType +template +struct is_pipeline_stage_type : std::false_type +{ +}; + +// PipelineRequestType +template +struct is_pipeline_request_type : std::false_type +{ +}; + +#ifdef BOOST_MYSQL_HAS_CONCEPTS + +// If you're getting errors pointing to this line, you're passing invalid stage types +// to a pipeline. Valid types include execute_stage, prepare_statement_stage, close_statement_stage, +// reset_connection_stage and set_character_set_stage +template +concept pipeline_stage_type = is_pipeline_stage_type::value; + +template +concept pipeline_request_type = is_pipeline_request_type::value; + +#define BOOST_MYSQL_PIPELINE_STAGE_TYPE ::boost::mysql::detail::pipeline_stage_type +#define BOOST_MYSQL_PIPELINE_REQUEST_TYPE ::boost::mysql::detail::pipeline_request_type + +#else + +#define BOOST_MYSQL_PIPELINE_STAGE_TYPE class +#define BOOST_MYSQL_PIPELINE_REQUEST_TYPE class + +#endif + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/detail/throw_on_error_loc.hpp b/include/boost/mysql/detail/throw_on_error_loc.hpp index ccf07655..5203cae3 100644 --- a/include/boost/mysql/detail/throw_on_error_loc.hpp +++ b/include/boost/mysql/detail/throw_on_error_loc.hpp @@ -18,7 +18,7 @@ namespace boost { namespace mysql { namespace detail { -inline void throw_on_error_loc(error_code err, const diagnostics& diag, const boost::source_location& loc) +inline void throw_on_error_loc(error_code err, const diagnostics& diag, const source_location& loc) { if (err) { diff --git a/include/boost/mysql/detail/writable_field_traits.hpp b/include/boost/mysql/detail/writable_field_traits.hpp index 0b583d5d..77efe25f 100644 --- a/include/boost/mysql/detail/writable_field_traits.hpp +++ b/include/boost/mysql/detail/writable_field_traits.hpp @@ -72,6 +72,19 @@ struct is_writable_field : std::integral_constant { }; +#ifdef BOOST_MYSQL_HAS_CONCEPTS + +template +concept writable_field = is_writable_field::value; + +#define BOOST_MYSQL_WRITABLE_FIELD ::boost::mysql::detail::writable_field + +#else + +#define BOOST_MYSQL_WRITABLE_FIELD class + +#endif + // field_view_forward_iterator template struct is_field_view_forward_iterator : std::false_type diff --git a/include/boost/mysql/error_with_diagnostics.hpp b/include/boost/mysql/error_with_diagnostics.hpp index f0f3dba9..de4a067e 100644 --- a/include/boost/mysql/error_with_diagnostics.hpp +++ b/include/boost/mysql/error_with_diagnostics.hpp @@ -11,6 +11,8 @@ #include #include +#include + #include namespace boost { @@ -22,20 +24,20 @@ namespace mysql { * Like `boost::system::system_error`, but adds a \ref diagnostics member * containing additional information. */ -class error_with_diagnostics : public boost::system::system_error +class error_with_diagnostics : public system::system_error { diagnostics diag_; - static boost::system::system_error create_base(const error_code& err, const diagnostics& diag) + static system::system_error create_base(const error_code& err, const diagnostics& diag) { - return diag.client_message().empty() ? boost::system::system_error(err) - : boost::system::system_error(err, diag.client_message()); + return diag.client_message().empty() ? system::system_error(err) + : system::system_error(err, diag.client_message()); } public: /// Initializing constructor. error_with_diagnostics(const error_code& err, const diagnostics& diag) - : boost::system::system_error(create_base(err, diag)), diag_(diag) + : system::system_error(create_base(err, diag)), diag_(diag) { } @@ -50,7 +52,38 @@ public: const diagnostics& get_diagnostics() const noexcept { return diag_; } }; +/** + * \brief A custom error type to be used with system::result. + * \details + * A custom error type containing an error code and a diagnostics object. + * It can be used as the second template parameter of `boost::system::result` + * (`boost::system::result`). + * \n + * When `system::result::value()` throws, the exception type is \ref error_with_diagnostics, + * containing the error code and diagnostics contained in this type. + */ +struct errcode_with_diagnostics +{ + /// The error code. + error_code code; + + /// The diagnostics object. + diagnostics diag; +}; + +// Required to make errcode_and_diagnostics compatible with boost::system +#ifndef BOOST_MYSQL_DOXYGEN +[[noreturn]] BOOST_MYSQL_DECL void throw_exception_from_error( + const errcode_with_diagnostics& e, + const source_location& loc +); +#endif + } // namespace mysql } // namespace boost +#ifdef BOOST_MYSQL_HEADER_ONLY +#include +#endif + #endif diff --git a/include/boost/mysql/impl/connection_impl.ipp b/include/boost/mysql/impl/connection_impl.ipp index f21ec782..82095005 100644 --- a/include/boost/mysql/impl/connection_impl.ipp +++ b/include/boost/mysql/impl/connection_impl.ipp @@ -98,6 +98,7 @@ BOOST_MYSQL_INSTANTIATE_SETUP(ping_algo_params) BOOST_MYSQL_INSTANTIATE_SETUP(reset_connection_algo_params) BOOST_MYSQL_INSTANTIATE_SETUP(quit_connection_algo_params) BOOST_MYSQL_INSTANTIATE_SETUP(close_connection_algo_params) +BOOST_MYSQL_INSTANTIATE_SETUP(run_pipeline_algo_params) BOOST_MYSQL_INSTANTIATE_GET_RESULT(read_some_rows_algo_params) BOOST_MYSQL_INSTANTIATE_GET_RESULT(read_some_rows_dynamic_algo_params) diff --git a/include/boost/mysql/impl/error_with_diagnostics.ipp b/include/boost/mysql/impl/error_with_diagnostics.ipp new file mode 100644 index 00000000..fcc48791 --- /dev/null +++ b/include/boost/mysql/impl/error_with_diagnostics.ipp @@ -0,0 +1,22 @@ +// +// Copyright (c) 2019-2024 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_ERROR_WITH_DIAGNOSTICS_IPP +#define BOOST_MYSQL_IMPL_ERROR_WITH_DIAGNOSTICS_IPP + +#pragma once + +#include + +#include + +void boost::mysql::throw_exception_from_error(const errcode_with_diagnostics& e, const source_location& loc) +{ + ::boost::throw_exception(error_with_diagnostics(e.code, e.diag), loc); +} + +#endif diff --git a/include/boost/mysql/impl/internal/connection_pool/connection_node.hpp b/include/boost/mysql/impl/internal/connection_pool/connection_node.hpp index 9f91d153..6c01e0b5 100644 --- a/include/boost/mysql/impl/internal/connection_pool/connection_node.hpp +++ b/include/boost/mysql/impl/internal/connection_pool/connection_node.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -57,6 +58,10 @@ struct conn_shared_state diagnostics last_diag; }; +// Helper to create the reset pipeline (reset + set names utf8mb4) +using reset_pipeline_req_t = static_pipeline_request; +inline reset_pipeline_req_t make_reset_pipeline() { return reset_pipeline_req_t({}, {utf8mb4_charset}); } + // The templated type is never exposed to the user. We template // so tests can inject mocks. template @@ -75,6 +80,8 @@ class basic_connection_node : public intrusive::list_base_hook<>, diagnostics connect_diag_; timer_type collection_timer_; // Notifications about collections. A separate timer makes potential race // conditions not harmful + const reset_pipeline_req_t* reset_pipeline_req_; + reset_pipeline_req_t::response_type reset_pipeline_res_; // Thread-safe std::atomic collection_state_{collection_state::none}; @@ -147,7 +154,11 @@ class basic_connection_node : public intrusive::list_base_hook<>, break; case next_connection_action::reset: run_with_timeout( - access::get_impl(node_.conn_).async_reset_with_charset(utf8mb4_charset, asio::deferred), + node_.conn_.async_run_pipeline( + *node_.reset_pipeline_req_, + node_.reset_pipeline_res_, + asio::deferred + ), node_.timer_, node_.params_->ping_timeout, std::move(self) @@ -172,13 +183,15 @@ public: internal_pool_params& params, boost::asio::any_io_executor ex, boost::asio::any_io_executor conn_ex, - conn_shared_state& shared_st + conn_shared_state& shared_st, + const reset_pipeline_req_t* reset_pipeline_req ) : params_(¶ms), shared_st_(&shared_st), conn_(std::move(conn_ex), params.make_ctor_params()), timer_(ex), - collection_timer_(ex, (std::chrono::steady_clock::time_point::max)()) + collection_timer_(ex, (std::chrono::steady_clock::time_point::max)()), + reset_pipeline_req_(reset_pipeline_req) { } @@ -191,8 +204,8 @@ public: // This initiation must be invoked within the pool's executor template - auto async_run(CompletionToken&& token) - -> decltype(asio::async_compose(connection_task_op{*this}, token)) + auto async_run(CompletionToken&& token + ) -> decltype(asio::async_compose(connection_task_op{*this}, token)) { return asio::async_compose(connection_task_op{*this}, token); } diff --git a/include/boost/mysql/impl/internal/connection_pool/connection_pool_impl.hpp b/include/boost/mysql/impl/internal/connection_pool/connection_pool_impl.hpp index decd206d..e4ac15d8 100644 --- a/include/boost/mysql/impl/internal/connection_pool/connection_pool_impl.hpp +++ b/include/boost/mysql/impl/internal/connection_pool/connection_pool_impl.hpp @@ -67,6 +67,7 @@ class basic_pool_impl : public std::enable_shared_from_this shared_from_this_wrapper() { @@ -76,7 +77,7 @@ class basic_pool_impl : public std::enable_shared_from_this #include -#include namespace boost { namespace mysql { @@ -106,8 +105,6 @@ BOOST_ATTRIBUTE_NODISCARD inline error_code deserialize_prepare_stmt_response( // Execution messages struct execute_response { - static_assert(std::is_trivially_destructible::value, ""); - enum class type_t { num_fields, diff --git a/include/boost/mysql/impl/internal/protocol/serialization.hpp b/include/boost/mysql/impl/internal/protocol/serialization.hpp index f8a0b430..afab0e2d 100644 --- a/include/boost/mysql/impl/internal/protocol/serialization.hpp +++ b/include/boost/mysql/impl/internal/protocol/serialization.hpp @@ -126,7 +126,7 @@ template inline std::uint8_t serialize_top_level( const Serializable& input, std::vector& to, - std::uint8_t seqnum, + std::uint8_t seqnum = 0, std::size_t frame_size = max_packet_size ) { diff --git a/include/boost/mysql/impl/internal/sansio/close_connection.hpp b/include/boost/mysql/impl/internal/sansio/close_connection.hpp index 70030eee..6fd718d6 100644 --- a/include/boost/mysql/impl/internal/sansio/close_connection.hpp +++ b/include/boost/mysql/impl/internal/sansio/close_connection.hpp @@ -12,26 +12,23 @@ #include #include +#include #include -#include namespace boost { namespace mysql { namespace detail { -class close_connection_algo : public sansio_algorithm +class close_connection_algo { int resume_point_{0}; quit_connection_algo quit_; error_code stored_ec_; public: - close_connection_algo(connection_state_data& st, close_connection_algo_params params) noexcept - : sansio_algorithm(st), quit_(st, {params.diag}) - { - } + close_connection_algo(close_connection_algo_params params) noexcept : quit_({params.diag}) {} - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { next_action act; @@ -43,11 +40,11 @@ public: quit_.diag().clear(); // If we're not connected, we're done - if (!st_->is_connected) + if (!st.is_connected) return next_action(); // Attempt quit - while (!(act = quit_.resume(ec)).is_done()) + while (!(act = quit_.resume(st, ec)).is_done()) BOOST_MYSQL_YIELD(resume_point_, 1, act) stored_ec_ = act.error(); diff --git a/include/boost/mysql/impl/internal/sansio/close_statement.hpp b/include/boost/mysql/impl/internal/sansio/close_statement.hpp index e2d5dac3..281558bb 100644 --- a/include/boost/mysql/impl/internal/sansio/close_statement.hpp +++ b/include/boost/mysql/impl/internal/sansio/close_statement.hpp @@ -8,74 +8,36 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CLOSE_STATEMENT_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CLOSE_STATEMENT_HPP -#include -#include -#include - #include -#include +#include -#include -#include #include #include -#include namespace boost { namespace mysql { namespace detail { -class close_statement_algo : public sansio_algorithm +inline run_pipeline_algo_params setup_close_statement_pipeline( + connection_state_data& st, + close_statement_algo_params params +) { - int resume_point_{0}; - diagnostics* diag_; - std::uint32_t stmt_id_; - std::uint8_t close_seqnum_{0}; - std::uint8_t ping_seqnum_{0}; - -public: - close_statement_algo(connection_state_data& st, close_statement_algo_params params) noexcept - : sansio_algorithm(st), diag_(params.diag), stmt_id_(params.stmt_id) - { - } - - next_action resume(error_code ec) - { - if (ec) - return ec; - - switch (resume_point_) - { - case 0: - - // Clear diagnostics - diag_->clear(); - - // Compose the requests. We pipeline a ping with the close statement - // to force the server send a response. Otherwise, the client ends up waiting - // for the next TCP ACK, which takes some milliseconds to be sent - // (see https://github.com/boostorg/mysql/issues/181) - st_->writer.prepare_pipelined_write( - close_stmt_command{stmt_id_}, - close_seqnum_, - ping_command{}, - ping_seqnum_ - ); - BOOST_MYSQL_YIELD(resume_point_, 1, next_action::write({})) - - // Read ping response - BOOST_MYSQL_YIELD(resume_point_, 2, read(ping_seqnum_)) - - // Process the OK packet - return st_->deserialize_ok(*diag_); - } - - return next_action(); - } -}; + st.write_buffer.clear(); + auto seqnum1 = serialize_top_level(close_stmt_command{params.stmt_id}, st.write_buffer); + auto seqnum2 = serialize_top_level(ping_command{}, st.write_buffer); + st.shared_pipeline_stages = { + {{pipeline_stage_kind::close_statement, seqnum1, {}}, {pipeline_stage_kind::ping, seqnum2, {}}} + }; + return { + params.diag, + pipeline_request_view{st.write_buffer, st.shared_pipeline_stages}, + pipeline_response_ref{} + }; +} } // namespace detail } // namespace mysql } // namespace boost -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_CLOSE_STATEMENT_HPP_ */ +#endif diff --git a/include/boost/mysql/impl/internal/sansio/connect.hpp b/include/boost/mysql/impl/internal/sansio/connect.hpp index 68b13529..ae70444f 100644 --- a/include/boost/mysql/impl/internal/sansio/connect.hpp +++ b/include/boost/mysql/impl/internal/sansio/connect.hpp @@ -17,25 +17,24 @@ #include #include #include -#include namespace boost { namespace mysql { namespace detail { -class connect_algo : public sansio_algorithm +class connect_algo { int resume_point_{0}; handshake_algo handshake_; error_code stored_ec_; public: - connect_algo(connection_state_data& st, connect_algo_params params) noexcept - : sansio_algorithm(st), handshake_(st, {params.diag, params.hparams, params.secure_channel}) + connect_algo(connect_algo_params params) noexcept + : handshake_({params.diag, params.hparams, params.secure_channel}) { } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { next_action act; @@ -52,7 +51,7 @@ public: return ec; // Handshake - while (!(act = handshake_.resume(ec)).is_done()) + while (!(act = handshake_.resume(st, ec)).is_done()) BOOST_MYSQL_YIELD(resume_point_, 2, act) // If handshake failed, close the stream ignoring the result diff --git a/include/boost/mysql/impl/internal/sansio/connection_state.hpp b/include/boost/mysql/impl/internal/sansio/connection_state.hpp index 0ae49a09..e9260356 100644 --- a/include/boost/mysql/impl/internal/sansio/connection_state.hpp +++ b/include/boost/mysql/impl/internal/sansio/connection_state.hpp @@ -29,11 +29,11 @@ #include #include #include +#include #include #include #include -#include #include #include @@ -52,12 +52,10 @@ template <> struct get_algo { using type = read template <> struct get_algo { using type = read_some_rows_algo; }; 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; }; template <> struct get_algo { using type = close_connection_algo; }; +template <> struct get_algo { using type = run_pipeline_algo; }; template using get_algo_t = typename get_algo::type; // clang-format on @@ -76,12 +74,10 @@ class connection_state read_some_rows_algo, read_some_rows_dynamic_algo, prepare_statement_algo, - close_statement_algo, set_character_set_algo, - ping_algo, - reset_connection_algo, quit_connection_algo, - close_connection_algo>; + close_connection_algo, + run_pipeline_algo>; connection_state_data st_data_; any_algo algo_; @@ -92,12 +88,15 @@ public: // the need for a special null algo connection_state(std::size_t read_buffer_size, bool transport_supports_ssl) : st_data_(read_buffer_size, transport_supports_ssl), - algo_(top_level_algo(st_data_, ping_algo_params{&st_data_.shared_diag})) + algo_(top_level_algo( + st_data_, + quit_connection_algo_params{&st_data_.shared_diag} + )) { } - const connection_state_data& data() const noexcept { return st_data_; } - connection_state_data& data() noexcept { return st_data_; } + const connection_state_data& data() const { return st_data_; } + connection_state_data& data() { return st_data_; } template any_resumable_ref setup(AlgoParams params) @@ -105,11 +104,22 @@ public: return any_resumable_ref(algo_.emplace>>(st_data_, params)); } + any_resumable_ref setup(close_statement_algo_params params) + { + return setup(setup_close_statement_pipeline(st_data_, params)); + } + + any_resumable_ref setup(reset_connection_algo_params params) + { + return setup(setup_reset_connection_pipeline(st_data_, params)); + } + + any_resumable_ref setup(ping_algo_params params) { return setup(setup_ping_pipeline(st_data_, params)); } + template typename AlgoParams::result_type result() const { - static_assert(!has_void_result(), "Internal error"); - return variant2::get>>(algo_).inner_algo().result(); + return variant2::get>>(algo_).inner_algo().result(st_data_); } }; 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 74d38aaf..82c768c2 100644 --- a/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp +++ b/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp @@ -13,12 +13,17 @@ #include #include +#include +#include + #include #include +#include #include -#include +#include #include +#include #include namespace boost { @@ -50,6 +55,9 @@ struct connection_state_data // Temporary field storage, re-used by several ops std::vector shared_fields; + // Temporary pipeline stage storage, re-used by several ops + std::array shared_pipeline_stages; + // Do we want to retain metadata strings or not? Used to save allocations metadata_mode meta_mode{metadata_mode::minimal}; @@ -63,14 +71,16 @@ struct connection_state_data // The current character set, or a default-constructed character set (will all nullptrs) if unknown character_set current_charset{}; - // Reader and writer + // The write buffer + std::vector write_buffer; + + // Reader message_reader reader; - message_writer writer; - bool ssl_active() const noexcept { return ssl == ssl_state::active; } - bool supports_ssl() const noexcept { return ssl != ssl_state::unsupported; } + bool ssl_active() const { return ssl == ssl_state::active; } + bool supports_ssl() const { return ssl != ssl_state::unsupported; } - const character_set* charset_ptr() const noexcept + const character_set* charset_ptr() const { return current_charset.name.empty() ? nullptr : ¤t_charset; } @@ -95,10 +105,27 @@ struct connection_state_data } // Reads an OK packet from the reader. This operation is repeated in several places. - error_code deserialize_ok(diagnostics& diag) noexcept + error_code deserialize_ok(diagnostics& diag) { return deserialize_ok_response(reader.message(), flavor, diag, backslash_escapes); } + + // Helpers for sans-io algorithms + next_action read(std::uint8_t& seqnum, bool keep_parsing_state = false) + { + // buffer is attached by top_level_algo + reader.prepare_read(seqnum, keep_parsing_state); + return next_action::read({}); + } + + template + next_action write(const Serializable& msg, std::uint8_t& seqnum) + { + // use_ssl is attached by top_level_algo + write_buffer.clear(); + seqnum = serialize_top_level(msg, write_buffer, seqnum); + return next_action::write({write_buffer, false}); + } }; } // namespace detail diff --git a/include/boost/mysql/impl/internal/sansio/execute.hpp b/include/boost/mysql/impl/internal/sansio/execute.hpp index 3bcff1ac..a5b3bec6 100644 --- a/include/boost/mysql/impl/internal/sansio/execute.hpp +++ b/include/boost/mysql/impl/internal/sansio/execute.hpp @@ -19,33 +19,78 @@ #include #include #include -#include #include namespace boost { namespace mysql { namespace detail { -class execute_algo : public sansio_algorithm +class read_execute_response_algo { int resume_point_{0}; - start_execution_algo start_execution_st_; read_resultset_head_algo read_head_st_; read_some_rows_algo read_some_rows_st_; - diagnostics& diag() noexcept { return *read_head_st_.params().diag; } - execution_processor& processor() noexcept { return *read_head_st_.params().proc; } - public: - execute_algo(connection_state_data& st, execute_algo_params params) noexcept - : sansio_algorithm(st), - start_execution_st_(st, start_execution_algo_params{params.diag, params.req, params.proc}), - read_head_st_(st, read_resultset_head_algo_params{params.diag, params.proc}), - read_some_rows_st_(st, read_some_rows_algo_params{params.diag, params.proc, output_ref()}) + read_execute_response_algo(diagnostics* diag, execution_processor* proc) noexcept + : read_head_st_(read_resultset_head_algo_params{diag, proc}), + read_some_rows_st_(read_some_rows_algo_params{diag, proc, output_ref()}) { } - next_action resume(error_code ec) + diagnostics& diag() { return read_head_st_.diag(); } + execution_processor& processor() { return read_head_st_.processor(); } + + next_action resume(connection_state_data& st, error_code ec) + { + next_action act; + + switch (resume_point_) + { + case 0: + + while (!processor().is_complete()) + { + if (processor().is_reading_head()) + { + read_head_st_.reset(); + while (!(act = read_head_st_.resume(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 1, act) + if (act.error()) + return act; + } + else if (processor().is_reading_rows()) + { + read_some_rows_st_.reset(); + while (!(act = read_some_rows_st_.resume(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 2, act) + if (act.error()) + return act; + } + } + } + + return next_action(); + } +}; + +class execute_algo +{ + int resume_point_{0}; + start_execution_algo start_execution_st_; + read_execute_response_algo read_response_st_; + + diagnostics& diag() { return read_response_st_.diag(); } + execution_processor& processor() { return read_response_st_.processor(); } + +public: + execute_algo(execute_algo_params params) noexcept + : start_execution_st_(start_execution_algo_params{params.diag, params.req, params.proc}), + read_response_st_(params.diag, params.proc) + { + } + + next_action resume(connection_state_data& st, error_code ec) { next_action act; @@ -54,31 +99,15 @@ public: case 0: // Send request and read the first response - while (!(act = start_execution_st_.resume(ec)).is_done()) + while (!(act = start_execution_st_.resume(st, ec)).is_done()) BOOST_MYSQL_YIELD(resume_point_, 1, act) if (act.error()) return act; // Read anything else - while (!processor().is_complete()) - { - if (processor().is_reading_head()) - { - read_head_st_ = read_resultset_head_algo(*st_, read_head_st_.params()); - while (!(act = read_head_st_.resume(ec)).is_done()) - BOOST_MYSQL_YIELD(resume_point_, 2, act) - if (act.error()) - return act; - } - else if (processor().is_reading_rows()) - { - read_some_rows_st_ = read_some_rows_algo(*st_, read_some_rows_st_.params()); - while (!(act = read_some_rows_st_.resume(ec)).is_done()) - BOOST_MYSQL_YIELD(resume_point_, 3, act) - if (act.error()) - return act; - } - } + while (!(act = read_response_st_.resume(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 2, act) + return act; } return next_action(); diff --git a/include/boost/mysql/impl/internal/sansio/handshake.hpp b/include/boost/mysql/impl/internal/sansio/handshake.hpp index 08bbb408..82fef164 100644 --- a/include/boost/mysql/impl/internal/sansio/handshake.hpp +++ b/include/boost/mysql/impl/internal/sansio/handshake.hpp @@ -25,7 +25,6 @@ #include #include #include -#include #include @@ -41,10 +40,11 @@ inline capabilities conditional_capability(bool condition, std::uint32_t cap) inline error_code process_capabilities( const handshake_params& params, const server_hello& hello, - capabilities& negotiated_caps + capabilities& negotiated_caps, + bool transport_supports_ssl ) { - auto ssl = params.ssl(); + auto ssl = transport_supports_ssl ? params.ssl() : ssl_mode::disable; capabilities server_caps = hello.server_capabilities; capabilities required_caps = mandatory_capabilities | conditional_capability(!params.database().empty(), CLIENT_CONNECT_WITH_DB) | @@ -65,7 +65,7 @@ inline error_code process_capabilities( return error_code(); } -class handshake_algo : public sansio_algorithm +class handshake_algo { int resume_point_{0}; diagnostics* diag_; @@ -77,7 +77,7 @@ class handshake_algo : public sansio_algorithm // 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 + static character_set collation_id_to_charset(std::uint16_t collation_id) { switch (collation_id) { @@ -90,9 +90,9 @@ class handshake_algo : public sansio_algorithm } // 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); } + bool use_ssl(const connection_state_data& st) const { return st.current_capabilities.has(CLIENT_SSL); } - error_code process_handshake(span buffer) + error_code process_handshake(connection_state_data& st, span buffer) { // Deserialize server hello server_hello hello{}; @@ -102,16 +102,16 @@ class handshake_algo : public sansio_algorithm // Check capabilities capabilities negotiated_caps; - err = process_capabilities(hparams_, hello, negotiated_caps); + err = process_capabilities(hparams_, hello, negotiated_caps, st.supports_ssl()); if (err) return err; // Set capabilities & db flavor - st_->current_capabilities = negotiated_caps; - st_->flavor = hello.server; + st.current_capabilities = negotiated_caps; + st.flavor = hello.server; // If we're using SSL, mark the channel as secure - secure_channel_ = secure_channel_ || use_ssl(); + secure_channel_ = secure_channel_ || use_ssl(st); // Compute auth response return compute_auth_response( @@ -124,19 +124,19 @@ class handshake_algo : public sansio_algorithm } // Response to that initial greeting - ssl_request compose_ssl_request() + ssl_request compose_ssl_request(const connection_state_data& st) { return ssl_request{ - st_->current_capabilities, + st.current_capabilities, static_cast(max_packet_size), hparams_.connection_collation(), }; } - login_request compose_login_request() + login_request compose_login_request(const connection_state_data& st) { return login_request{ - st_->current_capabilities, + st.current_capabilities, static_cast(max_packet_size), hparams_.connection_collation(), hparams_.username(), @@ -170,48 +170,37 @@ class handshake_algo : public sansio_algorithm } // Composes an auth_switch_response message with the contents of auth_resp_ - auth_switch_response compose_auth_switch_response() const noexcept + auth_switch_response compose_auth_switch_response() const { return auth_switch_response{auth_resp_.data}; } - void on_success(const ok_view& ok) + void on_success(connection_state_data& st, const ok_view& ok) { - st_->is_connected = true; - st_->backslash_escapes = ok.backslash_escapes(); - st_->current_charset = collation_id_to_charset(hparams_.connection_collation()); + st.is_connected = true; + st.backslash_escapes = ok.backslash_escapes(); + st.current_charset = collation_id_to_charset(hparams_.connection_collation()); } - error_code process_ok() + error_code process_ok(connection_state_data& st) { ok_view res{}; - auto ec = deserialize_ok_packet(st_->reader.message(), res); + auto ec = deserialize_ok_packet(st.reader.message(), res); if (ec) return ec; - on_success(res); + on_success(st, res); return error_code(); } - static handshake_params fix_ssl_mode(const handshake_params& input, bool transport_supports_ssl) noexcept - { - handshake_params res(input); - if (!transport_supports_ssl) - res.set_ssl(ssl_mode::disable); - return res; - } - public: - handshake_algo(connection_state_data& st, handshake_algo_params params) noexcept - : sansio_algorithm(st), - diag_(params.diag), - hparams_(fix_ssl_mode(params.hparams, st.supports_ssl())), - secure_channel_(params.secure_channel) + handshake_algo(handshake_algo_params params) noexcept + : diag_(params.diag), hparams_(params.hparams), secure_channel_(params.secure_channel) { } - diagnostics& diag() noexcept { return *diag_; } + diagnostics& diag() { return *diag_; } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { if (ec) return ec; @@ -224,44 +213,44 @@ public: // Setup diag_->clear(); - st_->reset(); + st.reset(); // Read server greeting - BOOST_MYSQL_YIELD(resume_point_, 1, read(sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 1, st.read(sequence_number_)) // Process server greeting - ec = process_handshake(st_->reader.message()); + ec = process_handshake(st, st.reader.message()); if (ec) return ec; // SSL - if (use_ssl()) + if (use_ssl(st)) { // Send SSL request - BOOST_MYSQL_YIELD(resume_point_, 2, write(compose_ssl_request(), sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 2, st.write(compose_ssl_request(st), sequence_number_)) // SSL handshake BOOST_MYSQL_YIELD(resume_point_, 3, next_action::ssl_handshake()) // Mark the connection as using ssl - st_->ssl = ssl_state::active; + st.ssl = ssl_state::active; } // Compose and send handshake response - BOOST_MYSQL_YIELD(resume_point_, 4, write(compose_login_request(), sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 4, st.write(compose_login_request(st), sequence_number_)) // Auth message exchange while (true) { // Receive response - BOOST_MYSQL_YIELD(resume_point_, 5, read(sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 5, st.read(sequence_number_)) // Process it - resp = deserialize_handshake_server_response(st_->reader.message(), st_->flavor, *diag_); + resp = deserialize_handshake_server_response(st.reader.message(), st.flavor, *diag_); if (resp.type == handhake_server_response::type_t::ok) { // Auth success, quit - on_success(resp.data.ok); + on_success(st, resp.data.ok); return next_action(); } else if (resp.type == handhake_server_response::type_t::error) @@ -279,17 +268,17 @@ public: BOOST_MYSQL_YIELD( resume_point_, 6, - write(compose_auth_switch_response(), sequence_number_) + st.write(compose_auth_switch_response(), sequence_number_) ) } else if (resp.type == handhake_server_response::type_t::ok_follows) { // The next packet must be an OK packet. Read it - BOOST_MYSQL_YIELD(resume_point_, 7, read(sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 7, st.read(sequence_number_)) // Process it // Regardless of whether we succeeded or not, we're done - return process_ok(); + return process_ok(st); } else { @@ -304,7 +293,7 @@ public: BOOST_MYSQL_YIELD( resume_point_, 8, - write(compose_auth_switch_response(), sequence_number_) + st.write(compose_auth_switch_response(), sequence_number_) ) } } diff --git a/include/boost/mysql/impl/internal/sansio/message_reader.hpp b/include/boost/mysql/impl/internal/sansio/message_reader.hpp index 5ea51390..dc3ceeb4 100644 --- a/include/boost/mysql/impl/internal/sansio/message_reader.hpp +++ b/include/boost/mysql/impl/internal/sansio/message_reader.hpp @@ -41,7 +41,7 @@ public: { } - void reset() noexcept + void reset() { buffer_.reset(); state_ = parse_state(); @@ -50,7 +50,7 @@ public: // Prepares a read operation. sequence_number should be kept alive until // the next read is prepared or no more calls to resume() are expected. // If keep_state=true, and the op is not complete, parsing state is preserved - void prepare_read(std::uint8_t& sequence_number, bool keep_state = false) noexcept + void prepare_read(std::uint8_t& sequence_number, bool keep_state = false) { if (!keep_state || done()) state_ = parse_state(sequence_number); @@ -63,7 +63,7 @@ public: bool done() const { return state_.resume_point == -1; } // Returns any errors generated during parsing. Requires this->done() - error_code error() const noexcept + error_code error() const { BOOST_ASSERT(done()); return state_.ec; @@ -71,7 +71,7 @@ public: // Returns the last parsed message. Valid until prepare_buffer() // is next called. Requires done() && !error() - span message() const noexcept + span message() const { BOOST_ASSERT(done()); BOOST_ASSERT(!error()); @@ -79,7 +79,7 @@ public: } // Returns buffer space suitable to read bytes to - span buffer() noexcept { return buffer_.free_area(); } + span buffer() { return buffer_.free_area(); } // Removes old messages stored in the buffer, and resizes it, if required, to accomodate // the message currently being parsed. @@ -164,7 +164,7 @@ public: } // Exposed for testing - const read_buffer& internal_buffer() const noexcept { return buffer_; } + const read_buffer& internal_buffer() const { return buffer_; } private: read_buffer buffer_; @@ -184,7 +184,7 @@ private: parse_state(std::uint8_t& seqnum) noexcept : sequence_number(&seqnum) {} } state_; - void set_required_size(std::size_t required_bytes) noexcept + void set_required_size(std::size_t required_bytes) { if (required_bytes > buffer_.pending_size()) state_.required_size = required_bytes - buffer_.pending_size(); diff --git a/include/boost/mysql/impl/internal/sansio/message_writer.hpp b/include/boost/mysql/impl/internal/sansio/message_writer.hpp deleted file mode 100644 index 11181b26..00000000 --- a/include/boost/mysql/impl/internal/sansio/message_writer.hpp +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright (c) 2019-2024 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_MESSAGE_WRITER_HPP -#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_MESSAGE_WRITER_HPP - -#include - -#include -#include - -#include -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -class message_writer -{ - std::vector buffer_; - std::size_t offset_{0}; - - void reset() - { - buffer_.clear(); - offset_ = 0; - } - -public: - message_writer() = default; - - template - void prepare_write(const Serializable& message, std::uint8_t& sequence_number) - { - reset(); - sequence_number = serialize_top_level(message, buffer_, sequence_number); - resume(0); - } - - // Serializes two messages into the write buffer. They must fit in a single frame - template - void prepare_pipelined_write( - const Serializable1& msg1, - std::uint8_t& seqnum1, - const Serializable2& msg2, - std::uint8_t& seqnum2 - ) - { - reset(); - seqnum1 = serialize_top_level(msg1, buffer_, seqnum1); - seqnum2 = serialize_top_level(msg2, buffer_, seqnum2); - resume(0); - } - - bool done() const noexcept { return offset_ == buffer_.size(); } - - span current_chunk() const - { - BOOST_ASSERT(!done()); - BOOST_ASSERT(!buffer_.empty()); - return span(buffer_).subspan(offset_); - } - - void resume(std::size_t n) - { - std::size_t remaining = static_cast(buffer_.size() - offset_); - BOOST_ASSERT(n <= remaining); - ignore_unused(remaining); - offset_ += n; - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -#endif diff --git a/include/boost/mysql/impl/internal/sansio/ping.hpp b/include/boost/mysql/impl/internal/sansio/ping.hpp index bf7b5c77..fbb9e9e1 100644 --- a/include/boost/mysql/impl/internal/sansio/ping.hpp +++ b/include/boost/mysql/impl/internal/sansio/ping.hpp @@ -8,59 +8,56 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_PING_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_PING_HPP -#include -#include - #include +#include #include +#include #include -#include - -#include namespace boost { namespace mysql { namespace detail { -class ping_algo : public sansio_algorithm +class read_ping_response_algo { int resume_point_{0}; diagnostics* diag_; std::uint8_t seqnum_{0}; public: - ping_algo(connection_state_data& st, ping_algo_params params) noexcept - : sansio_algorithm(st), diag_(params.diag) - { - } + read_ping_response_algo(diagnostics* diag, std::uint8_t seqnum) noexcept : diag_(diag), seqnum_(seqnum) {} - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { - if (ec) - return ec; - switch (resume_point_) { case 0: - // Clear diagnostics - diag_->clear(); - - // Send the request - BOOST_MYSQL_YIELD(resume_point_, 1, write(ping_command(), seqnum_)) - - // Read the response - BOOST_MYSQL_YIELD(resume_point_, 2, read(seqnum_)) + // Issue a read + BOOST_MYSQL_YIELD(resume_point_, 1, st.read(seqnum_)) + if (ec) + return ec; // Process the OK packet - return st_->deserialize_ok(*diag_); + return st.deserialize_ok(*diag_); } - return next_action(); } }; +inline run_pipeline_algo_params setup_ping_pipeline(connection_state_data& st, ping_algo_params params) +{ + st.write_buffer.clear(); + auto seqnum = serialize_top_level(ping_command{}, st.write_buffer); + st.shared_pipeline_stages[0] = {pipeline_stage_kind::ping, seqnum, {}}; + return { + params.diag, + pipeline_request_view{st.write_buffer, {st.shared_pipeline_stages.data(), 1}}, + pipeline_response_ref{} + }; +} + } // namespace detail } // namespace mysql } // namespace boost diff --git a/include/boost/mysql/impl/internal/sansio/prepare_statement.hpp b/include/boost/mysql/impl/internal/sansio/prepare_statement.hpp index 30b02b2c..9947a5b4 100644 --- a/include/boost/mysql/impl/internal/sansio/prepare_statement.hpp +++ b/include/boost/mysql/impl/internal/sansio/prepare_statement.hpp @@ -11,31 +11,30 @@ #include #include #include -#include #include +#include #include +#include #include -#include namespace boost { namespace mysql { namespace detail { -class prepare_statement_algo : public sansio_algorithm +class read_prepare_statement_response_algo { int resume_point_{0}; diagnostics* diag_; - string_view stmt_sql_; std::uint8_t sequence_number_{0}; unsigned remaining_meta_{0}; statement res_; - error_code process_response() + error_code process_response(connection_state_data& st) { prepare_stmt_response response{}; - auto err = deserialize_prepare_stmt_response(st_->reader.message(), st_->flavor, response, *diag_); + auto err = deserialize_prepare_stmt_response(st.reader.message(), st.flavor, response, *diag_); if (err) return err; res_ = access::construct(response.id, response.num_params); @@ -44,12 +43,15 @@ class prepare_statement_algo : public sansio_algorithm } public: - prepare_statement_algo(connection_state_data& st, prepare_statement_algo_params params) noexcept - : sansio_algorithm(st), diag_(params.diag), stmt_sql_(params.stmt_sql) + read_prepare_statement_response_algo(diagnostics* diag, std::uint8_t seqnum) noexcept + : diag_(diag), sequence_number_(seqnum) { } - next_action resume(error_code ec) + std::uint8_t& sequence_number() { return sequence_number_; } + diagnostics& diag() { return *diag_; } + + next_action resume(connection_state_data& st, error_code ec) { if (ec) return ec; @@ -58,34 +60,74 @@ public: { case 0: - // Clear diagnostics - diag_->clear(); - - // Send request - BOOST_MYSQL_YIELD(resume_point_, 1, write(prepare_stmt_command{stmt_sql_}, sequence_number_)) + // Note: diagnostics should have been cleaned by other algos // Read response - BOOST_MYSQL_YIELD(resume_point_, 2, read(sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 1, st.read(sequence_number_)) // Process response - ec = process_response(); + ec = process_response(st); if (ec) return ec; // Server sends now one packet per parameter and field. // We ignore these for now. for (; remaining_meta_ > 0u; --remaining_meta_) - BOOST_MYSQL_YIELD(resume_point_, 3, read(sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 2, st.read(sequence_number_)) } return next_action(); } - statement result() const { return res_; } + statement result(const connection_state_data&) const { return res_; } +}; + +class prepare_statement_algo +{ + int resume_point_{0}; + read_prepare_statement_response_algo read_response_st_; + string_view stmt_sql_; + +public: + prepare_statement_algo(prepare_statement_algo_params params) noexcept + : read_response_st_(params.diag, 0u), stmt_sql_(params.stmt_sql) + { + } + + next_action resume(connection_state_data& st, error_code ec) + { + next_action act; + + switch (resume_point_) + { + case 0: + + // Clear diagnostics + read_response_st_.diag().clear(); + + // Send request + BOOST_MYSQL_YIELD( + resume_point_, + 1, + st.write(prepare_stmt_command{stmt_sql_}, read_response_st_.sequence_number()) + ) + if (ec) + return ec; + + // Read response + while (!(act = read_response_st_.resume(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 2, act) + return act; + } + + return next_action(); + } + + statement result(const connection_state_data& st) const { return read_response_st_.result(st); } }; } // namespace detail } // namespace mysql } // namespace boost -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_PREPARE_STATEMENT_HPP_ */ +#endif diff --git a/include/boost/mysql/impl/internal/sansio/quit_connection.hpp b/include/boost/mysql/impl/internal/sansio/quit_connection.hpp index 4e825fbd..66c1a136 100644 --- a/include/boost/mysql/impl/internal/sansio/quit_connection.hpp +++ b/include/boost/mysql/impl/internal/sansio/quit_connection.hpp @@ -17,27 +17,23 @@ #include #include #include -#include namespace boost { namespace mysql { namespace detail { -class quit_connection_algo : public sansio_algorithm +class quit_connection_algo { int resume_point_{0}; diagnostics* diag_; std::uint8_t sequence_number_{0}; public: - quit_connection_algo(connection_state_data& st, quit_connection_algo_params params) noexcept - : sansio_algorithm(st), diag_(params.diag) - { - } + quit_connection_algo(quit_connection_algo_params params) noexcept : diag_(params.diag) {} diagnostics& diag() { return *diag_; } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { switch (resume_point_) { @@ -47,20 +43,20 @@ public: diag_->clear(); // Send quit message - BOOST_MYSQL_YIELD(resume_point_, 1, write(quit_command(), sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 1, st.write(quit_command(), sequence_number_)) // Mark the session as finished, regardless of the write result - st_->is_connected = false; + st.is_connected = false; // If write resulted in an error, return if (ec) return ec; // Shutdown SSL. MySQL doesn't always shut down SSL correctly, so we ignore this error. - if (st_->ssl == ssl_state::active) + if (st.ssl == ssl_state::active) { BOOST_MYSQL_YIELD(resume_point_, 2, next_action::ssl_shutdown()) - st_->ssl = ssl_state::torn_down; + st.ssl = ssl_state::torn_down; } // Done diff --git a/include/boost/mysql/impl/internal/sansio/read_resultset_head.hpp b/include/boost/mysql/impl/internal/sansio/read_resultset_head.hpp index 5cf00939..c2477dad 100644 --- a/include/boost/mysql/impl/internal/sansio/read_resultset_head.hpp +++ b/include/boost/mysql/impl/internal/sansio/read_resultset_head.hpp @@ -16,7 +16,6 @@ #include #include -#include namespace boost { namespace mysql { @@ -59,52 +58,60 @@ inline error_code process_field_definition( return proc.on_meta(coldef, diag); } -class read_resultset_head_algo : public sansio_algorithm +class read_resultset_head_algo { - int resume_point_{0}; - read_resultset_head_algo_params params_; + diagnostics* diag_; + execution_processor* proc_; + + struct state_t + { + int resume_point{0}; + } state_; public: - read_resultset_head_algo(connection_state_data& st, read_resultset_head_algo_params params) noexcept - : sansio_algorithm(st), params_(params) + read_resultset_head_algo(read_resultset_head_algo_params params) noexcept + : diag_(params.diag), proc_(params.proc) { } - read_resultset_head_algo_params params() const { return params_; } + void reset() { state_ = state_t{}; } - next_action resume(error_code ec) + diagnostics& diag() { return *diag_; } + execution_processor& processor() { return *proc_; } + + next_action resume(connection_state_data& st, error_code ec) { if (ec) return ec; - switch (resume_point_) + switch (state_.resume_point) { case 0: // Clear diagnostics - params_.diag->clear(); + diag_->clear(); // If we're not reading head, return - if (!params_.proc->is_reading_head()) + if (!proc_->is_reading_head()) return next_action(); // Read the response - BOOST_MYSQL_YIELD(resume_point_, 1, read(params_.proc->sequence_number())) + BOOST_MYSQL_YIELD(state_.resume_point, 1, st.read(proc_->sequence_number())) // Response may be: ok_packet, err_packet, local infile request // (not implemented), or response with fields - ec = process_execution_response(*st_, *params_.proc, st_->reader.message(), *params_.diag); + ec = process_execution_response(st, *proc_, st.reader.message(), *diag_); if (ec) return ec; // Read all of the field definitions - while (params_.proc->is_reading_meta()) + while (proc_->is_reading_meta()) { // Read a message - BOOST_MYSQL_YIELD(resume_point_, 2, read(params_.proc->sequence_number())) + BOOST_MYSQL_YIELD(state_.resume_point, 2, st.read(proc_->sequence_number())) // Process the metadata packet - ec = process_field_definition(*params_.proc, st_->reader.message(), *params_.diag); + ec = process_field_definition(*proc_, st.reader.message(), *diag_); if (ec) return ec; } diff --git a/include/boost/mysql/impl/internal/sansio/read_some_rows.hpp b/include/boost/mysql/impl/internal/sansio/read_some_rows.hpp index ba53f7e7..23039ebc 100644 --- a/include/boost/mysql/impl/internal/sansio/read_some_rows.hpp +++ b/include/boost/mysql/impl/internal/sansio/read_some_rows.hpp @@ -18,7 +18,6 @@ #include #include #include -#include #include @@ -26,11 +25,17 @@ namespace boost { namespace mysql { namespace detail { -class read_some_rows_algo : public sansio_algorithm +class read_some_rows_algo { - int resume_point_{0}; - read_some_rows_algo_params params_; - std::size_t rows_read_{0}; + diagnostics* diag_; + execution_processor* proc_; + output_ref output_; + + struct state_t + { + int resume_point{0}; + std::size_t rows_read{0}; + } state_; BOOST_ATTRIBUTE_NODISCARD static std::pair process_some_rows( connection_state_data& st, @@ -88,31 +93,32 @@ class read_some_rows_algo : public sansio_algorithm return {error_code(), read_rows}; } - execution_processor& processor() { return *params_.proc; } - public: - read_some_rows_algo(connection_state_data& st, read_some_rows_algo_params params) noexcept - : sansio_algorithm(st), params_(params) + read_some_rows_algo(read_some_rows_algo_params params) noexcept + : diag_(params.diag), proc_(params.proc), output_(params.output) { } - read_some_rows_algo_params params() const { return params_; } + void reset() { state_ = state_t{}; } - next_action resume(error_code ec) + const execution_processor& processor() const { return *proc_; } + execution_processor& processor() { return *proc_; } + + next_action resume(connection_state_data& st, error_code ec) { if (ec) return ec; - switch (resume_point_) + switch (state_.resume_point) { case 0: // Clear diagnostics - params_.diag->clear(); + diag_->clear(); // Clear any previous use of shared fields. // Required for the dynamic version to work. - st_->shared_fields.clear(); + st.shared_fields.clear(); // If we are not reading rows, return if (!processor().is_reading_rows()) @@ -120,17 +126,17 @@ public: // Read at least one message. Keep parsing state, in case a previous message // was parsed partially - BOOST_MYSQL_YIELD(resume_point_, 1, read(processor().sequence_number(), true)) + BOOST_MYSQL_YIELD(state_.resume_point, 1, st.read(proc_->sequence_number(), true)) // Process messages - std::tie(ec, rows_read_) = process_some_rows(*st_, processor(), params_.output, *params_.diag); + std::tie(ec, state_.rows_read) = process_some_rows(st, *proc_, output_, *diag_); return ec; } return next_action(); } - std::size_t result() const noexcept { return rows_read_; } + std::size_t result(const connection_state_data&) const { return state_.rows_read; } }; } // namespace detail diff --git a/include/boost/mysql/impl/internal/sansio/read_some_rows_dynamic.hpp b/include/boost/mysql/impl/internal/sansio/read_some_rows_dynamic.hpp index ded87965..1299b37a 100644 --- a/include/boost/mysql/impl/internal/sansio/read_some_rows_dynamic.hpp +++ b/include/boost/mysql/impl/internal/sansio/read_some_rows_dynamic.hpp @@ -29,16 +29,16 @@ namespace detail { class read_some_rows_dynamic_algo : public read_some_rows_algo { public: - read_some_rows_dynamic_algo(connection_state_data& st, read_some_rows_dynamic_algo_params params) noexcept - : read_some_rows_algo(st, read_some_rows_algo_params{params.diag, params.exec_st, output_ref()}) + read_some_rows_dynamic_algo(read_some_rows_dynamic_algo_params params) noexcept + : read_some_rows_algo(read_some_rows_algo_params{params.diag, params.exec_st, output_ref()}) { } - rows_view result() const noexcept + rows_view result(const connection_state_data& st) const { - std::size_t num_rows = read_some_rows_algo::result(); - std::size_t num_cols = static_cast(*params().proc).meta().size(); - return access::construct(st_->shared_fields.data(), num_rows * num_cols, num_cols); + std::size_t num_rows = read_some_rows_algo::result(st); + std::size_t num_cols = static_cast(processor()).meta().size(); + return access::construct(st.shared_fields.data(), num_rows * num_cols, num_cols); } }; diff --git a/include/boost/mysql/impl/internal/sansio/reset_connection.hpp b/include/boost/mysql/impl/internal/sansio/reset_connection.hpp index 6763497f..e4992d1d 100644 --- a/include/boost/mysql/impl/internal/sansio/reset_connection.hpp +++ b/include/boost/mysql/impl/internal/sansio/reset_connection.hpp @@ -8,122 +8,80 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_RESET_CONNECTION_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_RESET_CONNECTION_HPP -#include #include #include #include +#include #include #include #include #include -#include -#include namespace boost { namespace mysql { namespace detail { -class reset_connection_algo : public sansio_algorithm +class read_reset_connection_response_algo { int resume_point_{0}; diagnostics* diag_; - character_set charset_; - std::uint8_t reset_seqnum_{0}; - std::uint8_t set_names_seqnum_{0}; - error_code stored_ec_; - - // true if we need to pipeline a SET NAMES with the reset request - bool has_charset() const { return !charset_.name.empty(); } - - next_action compose_request() - { - if (has_charset()) - { - // Compose the SET NAMES statement - auto query = compose_set_names(charset_); - if (query.has_error()) - return query.error(); - - // Compose the pipeline - st_->writer.prepare_pipelined_write( - reset_connection_command{}, - reset_seqnum_, - query_command{query.value()}, - set_names_seqnum_ - ); - - // Success - return next_action::write({}); - } - else - { - // Just compose the reset connection request - return write(reset_connection_command{}, reset_seqnum_); - } - } + std::uint8_t seqnum_{0}; public: - reset_connection_algo(connection_state_data& st, reset_connection_algo_params params) noexcept - : sansio_algorithm(st), diag_(params.diag), charset_(params.charset) + read_reset_connection_response_algo(diagnostics* diag, std::uint8_t seqnum) noexcept + : diag_(diag), seqnum_(seqnum) { } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { - if (ec) - return ec; - switch (resume_point_) { case 0: - // Clear diagnostics - diag_->clear(); - - // Send the request - BOOST_MYSQL_YIELD(resume_point_, 1, compose_request()) - // Read the reset response - BOOST_MYSQL_YIELD(resume_point_, 2, read(reset_seqnum_)) + BOOST_MYSQL_YIELD(resume_point_, 1, st.read(seqnum_)) + if (ec) + return ec; // Verify it's what we expected - stored_ec_ = st_->deserialize_ok(*diag_); - - if (!stored_ec_) + ec = st.deserialize_ok(*diag_); + if (!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{}; + st.current_charset = character_set{}; } - if (has_charset()) - { - // We issued a SET NAMES too, read its response - BOOST_MYSQL_YIELD(resume_point_, 3, read(set_names_seqnum_)) - - // Verify it's what we expected. Don't overwrite diagnostics if reset failed - ec = st_->deserialize_ok(stored_ec_ ? st_->shared_diag : *diag_); - if (!ec) - { - // Set the character set to the new known value - st_->current_charset = charset_; - } - - // Set the return value if there is no error code already stored - if (!stored_ec_) - stored_ec_ = ec; - } - - return stored_ec_; + // Done + return ec; } return next_action(); } }; +inline run_pipeline_algo_params setup_reset_connection_pipeline( + connection_state_data& st, + reset_connection_algo_params params +) +{ + st.write_buffer.clear(); + st.shared_pipeline_stages[0] = { + pipeline_stage_kind::reset_connection, + serialize_top_level(reset_connection_command{}, st.write_buffer), + {} + }; + return { + params.diag, + pipeline_request_view{st.write_buffer, {st.shared_pipeline_stages.data(), 1}}, + pipeline_response_ref() + }; +} + } // namespace detail } // namespace mysql } // namespace boost diff --git a/include/boost/mysql/impl/internal/sansio/run_pipeline.hpp b/include/boost/mysql/impl/internal/sansio/run_pipeline.hpp new file mode 100644 index 00000000..e759e1bc --- /dev/null +++ b/include/boost/mysql/impl/internal/sansio/run_pipeline.hpp @@ -0,0 +1,214 @@ +// +// Copyright (c) 2019-2024 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_RUN_PIPELINE_HPP +#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_RUN_PIPELINE_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +class run_pipeline_algo +{ + union any_read_algo + { + std::nullptr_t nothing; + read_execute_response_algo execute; + read_prepare_statement_response_algo prepare_statement; + read_reset_connection_response_algo reset_connection; + read_ping_response_algo ping; + read_set_character_set_response_algo set_character_set; + + any_read_algo() noexcept : nothing{} {} + }; + + diagnostics* diag_; + span request_buffer_; + span stages_; + pipeline_response_ref response_; + + int resume_point_{0}; + std::size_t current_stage_index_{0}; + error_code pipeline_ec_; // Result of the entire operation + bool has_hatal_error_{}; // If true, fail further stages with pipeline_ec_ + any_read_algo read_response_algo_; + diagnostics temp_diag_; + + void setup_current_stage(const connection_state_data& st) + { + // Reset previous data + temp_diag_.clear(); + + // Setup read algo + auto stage = stages_[current_stage_index_]; + switch (stage.kind) + { + case pipeline_stage_kind::execute: + { + auto& processor = response_.get_processor(current_stage_index_); + processor.reset(stage.stage_specific.enc, st.meta_mode); + processor.sequence_number() = stage.seqnum; + read_response_algo_.execute = {&temp_diag_, &processor}; + break; + } + case pipeline_stage_kind::prepare_statement: + read_response_algo_.prepare_statement = {&temp_diag_, stage.seqnum}; + break; + case pipeline_stage_kind::close_statement: + // Close statement doesn't have a response + read_response_algo_.nothing = nullptr; + break; + case pipeline_stage_kind::set_character_set: + read_response_algo_.set_character_set = {&temp_diag_, stage.stage_specific.charset, stage.seqnum}; + break; + case pipeline_stage_kind::reset_connection: + read_response_algo_.reset_connection = {&temp_diag_, stage.seqnum}; + break; + case pipeline_stage_kind::ping: read_response_algo_.ping = {&temp_diag_, stage.seqnum}; break; + default: BOOST_ASSERT(false); + } + } + + void on_stage_finished(const connection_state_data& st, error_code stage_ec) + { + if (stage_ec) + { + // If the error was fatal, fail successive stages. + // This error is the result of the operation + if (is_fatal_error(stage_ec)) + { + pipeline_ec_ = stage_ec; + *diag_ = temp_diag_; + has_hatal_error_ = true; + } + else if (!pipeline_ec_) + { + // In the absence of fatal errors, the first error we encounter is the result of the operation + pipeline_ec_ = stage_ec; + *diag_ = temp_diag_; + } + + // Propagate the error + response_.set_error(current_stage_index_, stage_ec, std::move(temp_diag_)); + } + else + { + if (stages_[current_stage_index_].kind == pipeline_stage_kind::prepare_statement) + { + // Propagate results. We don't support prepare statements + // with responses having has_value() == false + response_.set_result(current_stage_index_, read_response_algo_.prepare_statement.result(st)); + } + } + } + + next_action resume_read_algo(connection_state_data& st, error_code ec) + { + switch (stages_[current_stage_index_].kind) + { + case pipeline_stage_kind::execute: return read_response_algo_.execute.resume(st, ec); + case pipeline_stage_kind::prepare_statement: + return read_response_algo_.prepare_statement.resume(st, ec); + case pipeline_stage_kind::reset_connection: + return read_response_algo_.reset_connection.resume(st, ec); + case pipeline_stage_kind::set_character_set: + return read_response_algo_.set_character_set.resume(st, ec); + case pipeline_stage_kind::ping: return read_response_algo_.ping.resume(st, ec); + case pipeline_stage_kind::close_statement: return next_action(); // has no response + default: BOOST_ASSERT(false); return next_action(); + } + } + +public: + run_pipeline_algo(run_pipeline_algo_params params) noexcept + : diag_(params.diag), + request_buffer_(params.request.buffer), + stages_(params.request.stages), + response_(params.response) + { + } + + next_action resume(connection_state_data& st, error_code ec) + { + next_action act; + + switch (resume_point_) + { + case 0: + + // Clear previous state + diag_->clear(); + response_.setup(stages_); + + // If the request is empty, don't do anything + if (stages_.empty()) + break; + + // Write the request. use_ssl is attached by top_level_algo + BOOST_MYSQL_YIELD(resume_point_, 1, next_action::write({request_buffer_, false})) + + // If writing the request failed, fail all the stages with the given error code + if (ec) + { + pipeline_ec_ = ec; + has_hatal_error_ = true; + } + + // For each stage + for (; current_stage_index_ < stages_.size(); ++current_stage_index_) + { + // If there was a fatal error, just set the error and move forward + if (has_hatal_error_) + { + response_.set_error(current_stage_index_, pipeline_ec_, diagnostics(*diag_)); + continue; + } + + // Setup the stage + setup_current_stage(st); + + // Run it until completion + ec.clear(); + while (!(act = resume_read_algo(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 2, act) + + // Process the stage's result + on_stage_finished(st, act.error()); + } + } + + return pipeline_ec_; + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/internal/sansio/sansio_algorithm.hpp b/include/boost/mysql/impl/internal/sansio/sansio_algorithm.hpp deleted file mode 100644 index c8e65f9a..00000000 --- a/include/boost/mysql/impl/internal/sansio/sansio_algorithm.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright (c) 2019-2024 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_SANSIO_ALGORITHM_HPP -#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_SANSIO_ALGORITHM_HPP - -#include - -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -class sansio_algorithm -{ -protected: - connection_state_data* st_; - - next_action read(std::uint8_t& seqnum, bool keep_parsing_state = false) - { - // buffer is attached by top_level_algo - st_->reader.prepare_read(seqnum, keep_parsing_state); - return next_action::read(next_action::read_args_t{{}, false}); - } - - template - next_action write(const Serializable& msg, std::uint8_t& seqnum) - { - // buffer is attached by top_level_algo - st_->writer.prepare_write(msg, seqnum); - return next_action::write(next_action::write_args_t{{}, false}); - } - - sansio_algorithm(connection_state_data& st) noexcept : st_(&st) {} - -public: - const connection_state_data& conn_state() const noexcept { return *st_; } - connection_state_data& conn_state() noexcept { return *st_; } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -#endif diff --git a/include/boost/mysql/impl/internal/sansio/set_character_set.hpp b/include/boost/mysql/impl/internal/sansio/set_character_set.hpp index 06139a4c..dcbf24ae 100644 --- a/include/boost/mysql/impl/internal/sansio/set_character_set.hpp +++ b/include/boost/mysql/impl/internal/sansio/set_character_set.hpp @@ -9,10 +9,9 @@ #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_SET_CHARACTER_SET_HPP #include +#include #include -#include #include -#include #include #include @@ -20,54 +19,96 @@ #include #include #include -#include +#include #include -#include +#include namespace boost { namespace mysql { namespace detail { -// Securely compose a SET NAMES statement. This function -// is also used by the pipelined version of reset_connection -inline system::result compose_set_names(const character_set& charset) +// Securely compose a SET NAMES statement +inline system::result compose_set_names(character_set charset) { - // The character set should have a non-empty name + // The character set should have a non-empty name (should not be default-constructed) BOOST_ASSERT(!charset.name.empty()); + // NULL characters cause MySQL to truncate the character set name string + if (charset.name.contains('\0')) + return client_errc::invalid_encoding; + // For security, if the character set has non-ascii characters in it name, reject it. format_context ctx(format_options{ascii_charset, true}); ctx.append_raw("SET NAMES ").append_value(charset.name); return std::move(ctx).get(); } -class set_character_set_algo : public sansio_algorithm +class read_set_character_set_response_algo { int resume_point_{0}; diagnostics* diag_; character_set charset_; std::uint8_t seqnum_{0}; - next_action compose_request() +public: + read_set_character_set_response_algo(diagnostics* diag, character_set charset, std::uint8_t seqnum) + : diag_(diag), charset_(charset), seqnum_(seqnum) { - auto q = compose_set_names(charset_); + } + character_set charset() const { return charset_; } + diagnostics& diag() { return *diag_; } + std::uint8_t& sequence_number() { return seqnum_; } + + next_action resume(connection_state_data& st, error_code 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. + switch (resume_point_) + { + case 0: + + // Read the response + BOOST_MYSQL_YIELD(resume_point_, 1, st.read(seqnum_)) + if (ec) + return ec; + + // Verify it's what we expected + ec = st.deserialize_ok(*diag_); + if (ec) + return ec; + + // If we were successful, update the character set + st.current_charset = charset_; + } + + return next_action(); + } +}; + +class set_character_set_algo +{ + int resume_point_{0}; + read_set_character_set_response_algo read_response_st_; + + next_action compose_request(connection_state_data& st) + { + auto q = compose_set_names(read_response_st_.charset()); if (q.has_error()) return q.error(); - return write(query_command{q.value()}, seqnum_); + return st.write(query_command{q.value()}, read_response_st_.sequence_number()); } 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) + set_character_set_algo(set_character_set_algo_params params) noexcept + : read_response_st_(params.diag, params.charset, 0u) { } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { - if (ec) - return ec; + next_action act; // 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. @@ -76,21 +117,17 @@ public: case 0: // Setup - diag_->clear(); + read_response_st_.diag().clear(); // Send the execution request - BOOST_MYSQL_YIELD(resume_point_, 1, compose_request()) - - // Read the response - BOOST_MYSQL_YIELD(resume_point_, 2, read(seqnum_)) - - // Verify it's what we expected - ec = st_->deserialize_ok(*diag_); + BOOST_MYSQL_YIELD(resume_point_, 1, compose_request(st)) if (ec) return ec; - // If we were successful, update the character set - st_->current_charset = charset_; + // Read the response + while (!(act = read_response_st_.resume(st, ec)).is_done()) + BOOST_MYSQL_YIELD(resume_point_, 2, act) + return act; } return next_action(); diff --git a/include/boost/mysql/impl/internal/sansio/start_execution.hpp b/include/boost/mysql/impl/internal/sansio/start_execution.hpp index c9558bd1..5d1bac14 100644 --- a/include/boost/mysql/impl/internal/sansio/start_execution.hpp +++ b/include/boost/mysql/impl/internal/sansio/start_execution.hpp @@ -20,7 +20,6 @@ #include #include #include -#include namespace boost { namespace mysql { @@ -39,37 +38,35 @@ inline resultset_encoding get_encoding(const any_execution_request& req) return req.is_query ? resultset_encoding::text : resultset_encoding::binary; } -class start_execution_algo : public sansio_algorithm +class start_execution_algo { int resume_point_{0}; read_resultset_head_algo read_head_st_; any_execution_request req_; std::uint8_t& seqnum() { return processor().sequence_number(); } - execution_processor& processor() { return *read_head_st_.params().proc; } - diagnostics& diag() { return *read_head_st_.params().diag; } + execution_processor& processor() { return read_head_st_.processor(); } + diagnostics& diag() { return read_head_st_.diag(); } - next_action compose_request() + next_action compose_request(connection_state_data& st) { if (req_.is_query) { - return write(query_command{req_.data.query}, seqnum()); + return st.write(query_command{req_.data.query}, seqnum()); } else { - return write(execute_stmt_command{req_.data.stmt.stmt.id(), req_.data.stmt.params}, seqnum()); + return st.write(execute_stmt_command{req_.data.stmt.stmt.id(), req_.data.stmt.params}, seqnum()); } } public: - start_execution_algo(connection_state_data& st, start_execution_algo_params params) noexcept - : sansio_algorithm(st), - read_head_st_(st, read_resultset_head_algo_params{params.diag, params.proc}), - req_(params.req) + start_execution_algo(start_execution_algo_params params) noexcept + : read_head_st_(read_resultset_head_algo_params{params.diag, params.proc}), req_(params.req) { } - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { next_action act; @@ -86,16 +83,16 @@ public: return ec; // Reset the processor - processor().reset(get_encoding(req_), st_->meta_mode); + processor().reset(get_encoding(req_), st.meta_mode); // Send the execution request - BOOST_MYSQL_YIELD(resume_point_, 1, compose_request()) + BOOST_MYSQL_YIELD(resume_point_, 1, compose_request(st)) if (ec) return ec; // Read the first resultset's head and return its result - while (!(act = read_head_st_.resume(ec)).is_done()) + while (!(act = read_head_st_.resume(st, ec)).is_done()) BOOST_MYSQL_YIELD(resume_point_, 2, act) return act; } diff --git a/include/boost/mysql/impl/internal/sansio/top_level_algo.hpp b/include/boost/mysql/impl/internal/sansio/top_level_algo.hpp index dbbea89b..75f0e4fb 100644 --- a/include/boost/mysql/impl/internal/sansio/top_level_algo.hpp +++ b/include/boost/mysql/impl/internal/sansio/top_level_algo.hpp @@ -14,9 +14,11 @@ #include #include -#include + +#include #include +#include #ifdef BOOST_USE_VALGRIND #include @@ -36,22 +38,25 @@ inline void valgrind_make_mem_defined(const void* data, std::size_t size) inline void valgrind_make_mem_defined(const void*, std::size_t) noexcept {} #endif +// InnerAlgo should have +// Constructible from the args forwarded by the ctor. +// next_action resume(connection_state_data&, error_code); +// AlgoParams::result_type result(const connection_state_data&) const; // if AlgoParams::result_type != void template class top_level_algo { int resume_point_{0}; + connection_state_data* st_; InnerAlgo algo_; - - connection_state_data& conn_state() noexcept { return algo_.conn_state(); } + span bytes_to_write_; public: template - top_level_algo(connection_state_data& st_data, Args&&... args) - : algo_(st_data, std::forward(args)...) + top_level_algo(connection_state_data& st, Args&&... args) : st_(&st), algo_(std::forward(args)...) { } - const InnerAlgo& inner_algo() const noexcept { return algo_; } + const InnerAlgo& inner_algo() const { return algo_; } next_action resume(error_code ec, std::size_t bytes_transferred) { @@ -65,48 +70,47 @@ public: while (true) { // Run the op - act = algo_.resume(ec); + act = algo_.resume(*st_, ec); // Check next action if (act.is_done()) { return act; } - else if (act.type() == next_action::type_t::read) + else if (act.type() == next_action_type::read) { // Read until a complete message is received // (may be zero times if cached) - while (!conn_state().reader.done() && !ec) + while (!st_->reader.done() && !ec) { - conn_state().reader.prepare_buffer(); + st_->reader.prepare_buffer(); BOOST_MYSQL_YIELD( resume_point_, 1, - next_action::read({conn_state().reader.buffer(), conn_state().ssl_active()}) + next_action::read({st_->reader.buffer(), st_->ssl_active()}) ) - valgrind_make_mem_defined(conn_state().reader.buffer().data(), bytes_transferred); - conn_state().reader.resume(bytes_transferred); + valgrind_make_mem_defined(st_->reader.buffer().data(), bytes_transferred); + st_->reader.resume(bytes_transferred); } // Check for errors if (!ec) - ec = conn_state().reader.error(); + ec = st_->reader.error(); // We've got a message, continue } - else if (act.type() == next_action::type_t::write) + else if (act.type() == next_action_type::write) { // Write until a complete message was written - while (!conn_state().writer.done() && !ec) + bytes_to_write_ = act.write_args().buffer; + while (!bytes_to_write_.empty() && !ec) { BOOST_MYSQL_YIELD( resume_point_, 2, - next_action::write( - {conn_state().writer.current_chunk(), conn_state().ssl_active()} - ) + next_action::write({bytes_to_write_, st_->ssl_active()}) ) - conn_state().writer.resume(bytes_transferred); + bytes_to_write_ = bytes_to_write_.subspan(bytes_transferred); } // We fully wrote a message, continue diff --git a/include/boost/mysql/impl/pipeline.hpp b/include/boost/mysql/impl/pipeline.hpp new file mode 100644 index 00000000..4a4e8862 --- /dev/null +++ b/include/boost/mysql/impl/pipeline.hpp @@ -0,0 +1,216 @@ +// +// Copyright (c) 2019-2024 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_PIPELINE_HPP +#define BOOST_MYSQL_IMPL_PIPELINE_HPP + +#pragma once + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Stage concepts +template <> +struct is_pipeline_stage_type : std::true_type +{ +}; + +template <> +struct is_pipeline_stage_type : std::true_type +{ +}; + +template <> +struct is_pipeline_stage_type : std::true_type +{ +}; + +template <> +struct is_pipeline_stage_type : std::true_type +{ +}; + +template <> +struct is_pipeline_stage_type : std::true_type +{ +}; + +// Request concepts +template <> +struct is_pipeline_request_type : std::true_type +{ +}; + +template +struct is_pipeline_request_type> : std::true_type +{ +}; + +// Response traits +template <> +struct pipeline_response_traits> +{ + using response_type = std::vector; + + static void setup(response_type& self, span request) + { + // Create as many response items as request stages + self.resize(request.size()); + + // Setup them + for (std::size_t i = 0u; i < request.size(); ++i) + { + // Execution stages need to be initialized to results objects. + // Otherwise, clear any previous content + auto& impl = access::get_impl(self[i]); + if (request[i].kind == pipeline_stage_kind::execute) + impl.emplace_results(); + else + impl.emplace_error(); + } + } + + static execution_processor& get_processor(response_type& self, std::size_t idx) + { + BOOST_ASSERT(idx < self.size()); + return access::get_impl(self[idx]).get_processor(); + } + + static void set_result(response_type& self, std::size_t idx, statement stmt) + { + BOOST_ASSERT(idx < self.size()); + access::get_impl(self[idx]).set_result(stmt); + } + + static void set_error(response_type& self, std::size_t idx, error_code ec, diagnostics&& diag) + { + BOOST_ASSERT(idx < self.size()); + access::get_impl(self[idx]).set_error(ec, std::move(diag)); + } +}; + +// Index a tuple at runtime +template +struct tuple_index_visitor +{ + template + static R invoke(std::tuple& t, std::size_t i, Fn f, Args&&... args) + { + return i == I ? f(std::get(t), std::forward(args)...) + : tuple_index_visitor::invoke(t, i, f, std::forward(args)...); + } +}; + +template +struct tuple_index_visitor<0u, R> +{ + template + static R invoke(std::tuple& t, std::size_t i, Fn f, Args&&... args) + { + BOOST_ASSERT(i == 0u); + ignore_unused(i); + return f(std::get<0u>(t), std::forward(args)...); + } +}; + +template +auto tuple_index(std::tuple& t, std::size_t i, Fn f, Args&&... args) + -> decltype(f(std::get<0u>(t), std::forward(args)...)) +{ + static_assert(sizeof...(T) > 0u, "Empty tuples not allowed"); + BOOST_ASSERT(i < sizeof...(T)); + using result_type = decltype(f(std::get<0u>(t), std::forward(args)...)); + return tuple_index_visitor::invoke(t, i, f, std::forward(args)...); +} + +// Concrete visitors +struct stage_reset_visitor +{ + void operator()(execute_stage::response_type& value) const { value.emplace(); } + void operator()(prepare_statement_stage::response_type& value) const { value.emplace(); } + + void operator()(errcode_with_diagnostics& value) const + { + value.code.clear(); + value.diag.clear(); + } +}; + +struct stage_get_processor_visitor +{ + execution_processor* operator()(execute_stage::response_type& value) const + { + return &access::get_impl(*value); + } + + template + execution_processor* operator()(PipelineStageType&) const + { + BOOST_ASSERT(false); + return nullptr; + } +}; + +struct stage_set_result_visitor +{ + void operator()(prepare_statement_stage::response_type& r, statement s) const { r = s; } + + template + void operator()(PipelineStageType&, statement) const + { + BOOST_ASSERT(false); + } +}; + +struct stage_set_error_visitor +{ + template + void operator()(PipelineStageType& r, error_code ec, diagnostics&& diag) const + { + r = errcode_with_diagnostics{ec, std::move(diag)}; + } +}; + +template +struct pipeline_response_traits> +{ + using response_type = std::tuple; + + static void setup(response_type& self, span request) + { + BOOST_ASSERT(request.size() == sizeof...(PipelineStageResponseType)); + ignore_unused(request); + mp11::tuple_for_each(self, stage_reset_visitor{}); + } + + static execution_processor& get_processor(response_type& self, std::size_t idx) + { + return *tuple_index(self, idx, stage_get_processor_visitor{}); + } + + static void set_result(response_type& self, std::size_t idx, statement stmt) + { + tuple_index(self, idx, stage_set_result_visitor{}, stmt); + } + + static void set_error(response_type& self, std::size_t idx, error_code ec, diagnostics&& diag) + { + tuple_index(self, idx, stage_set_error_visitor{}, ec, std::move(diag)); + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/pipeline.ipp b/include/boost/mysql/impl/pipeline.ipp new file mode 100644 index 00000000..3f2fe533 --- /dev/null +++ b/include/boost/mysql/impl/pipeline.ipp @@ -0,0 +1,179 @@ +// +// Copyright (c) 2019-2024 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_PIPELINE_IPP +#define BOOST_MYSQL_IMPL_PIPELINE_IPP + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +inline std::uint8_t serialize_execute_statement( + statement stmt, + span params, + std::vector& buff +) +{ + if (params.size() != stmt.num_params()) + { + BOOST_THROW_EXCEPTION( + std::invalid_argument("Wrong number of actual parameters supplied to a prepared statement") + ); + } + return serialize_top_level(execute_stmt_command{stmt.id(), params}, buff); +} + +// TODO: consider using Boost.Container (or adding tests) +inline std::uint8_t serialize_execute_stmt_tuple( + statement stmt, + span params, + std::vector& buff +) +{ + constexpr std::size_t stack_fields = 64u; + if (params.size() <= stack_fields) + { + std::array storage; + for (std::size_t i = 0; i < params.size(); ++i) + storage[i] = access::get_impl(params[i]); + return detail::serialize_execute_statement(stmt, {storage.data(), params.size()}, buff); + } + else + { + std::vector storage; + storage.reserve(params.size()); + for (auto p : params) + storage.push_back(access::get_impl(p)); + return detail::serialize_execute_statement(stmt, storage, buff); + } +} + +} // namespace detail +} // namespace mysql +} // namespace boost + +boost::mysql::detail::pipeline_request_stage boost::mysql::execute_stage::create( + std::vector& buff +) const +{ + switch (type_) + { + case type_query: + return { + detail::pipeline_stage_kind::execute, + detail::serialize_top_level(detail::query_command{data_.query}, buff), + detail::resultset_encoding::text + }; + case type_stmt_tuple: + { + return { + detail::pipeline_stage_kind::execute, + detail::serialize_execute_stmt_tuple(data_.stmt_tuple.stmt, data_.stmt_tuple.params, buff), + detail::resultset_encoding::binary + }; + } + case type_stmt_range: + return { + detail::pipeline_stage_kind::execute, + detail::serialize_execute_statement(data_.stmt_range.stmt, data_.stmt_range.params, buff), + detail::resultset_encoding::binary + }; + default: BOOST_ASSERT(false); return {}; + } +} + +boost::mysql::detail::pipeline_request_stage boost::mysql::prepare_statement_stage::create( + std::vector& buffer +) const +{ + return { + detail::pipeline_stage_kind::prepare_statement, + detail::serialize_top_level(detail::prepare_stmt_command{stmt_sql_}, buffer), + {} + }; +} + +boost::mysql::detail::pipeline_request_stage boost::mysql::close_statement_stage::create( + std::vector& buffer +) const +{ + return { + detail::pipeline_stage_kind::close_statement, + detail::serialize_top_level(detail::close_stmt_command{stmt_id_}, buffer), + {} + }; +} + +boost::mysql::detail::pipeline_request_stage boost::mysql::reset_connection_stage::create( + std::vector& buffer +) const +{ + return { + detail::pipeline_stage_kind::reset_connection, + detail::serialize_top_level(detail::reset_connection_command{}, buffer), + {} + }; +} + +boost::mysql::detail::pipeline_request_stage boost::mysql::set_character_set_stage::create( + std::vector& buffer +) const +{ + auto q = detail::compose_set_names(charset_); + if (q.has_error()) + { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid character set name")); + } + return { + detail::pipeline_stage_kind::set_character_set, + detail::serialize_top_level(detail::query_command{*q}, buffer), + charset_ + }; +} + +void boost::mysql::any_stage_response::check_has_results() const +{ + if (!has_results()) + { + BOOST_THROW_EXCEPTION( + std::invalid_argument("any_stage_response::as_results: object doesn't contain results") + ); + } +} + +boost::mysql::statement boost::mysql::any_stage_response::as_statement() const +{ + if (!has_statement()) + { + BOOST_THROW_EXCEPTION( + std::invalid_argument("any_stage_response::as_statement: object doesn't contain a statement") + ); + } + return variant2::unsafe_get<1>(impl_.value); +} + +#endif diff --git a/include/boost/mysql/pipeline.hpp b/include/boost/mysql/pipeline.hpp new file mode 100644 index 00000000..08a88016 --- /dev/null +++ b/include/boost/mysql/pipeline.hpp @@ -0,0 +1,796 @@ +// +// Copyright (c) 2019-2024 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_PIPELINE_HPP +#define BOOST_MYSQL_PIPELINE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace mysql { + +#ifndef BOOST_MYSQL_DOXYGEN +namespace detail { +struct pipeline_stage_access +{ + template + static pipeline_request_stage create(PipelineStageType stage, std::vector& buff) + { + return stage.create(buff); + } +}; +} // namespace detail +#endif + +/** + * \brief (EXPERIMENTAL) Pipeline stage type to execute text queries and prepared statements. + * \details + * To be used with \ref pipeline_request or \ref static_pipeline_request. + * Has effects equivalent to \ref any_connection::execute. + * + * \par Object lifetimes + * Like all stage types, this is a view type. Parameters required to run the stage + * are stored internally as views. Objects of this type should only be used as + * function or constructor parameters. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class execute_stage +{ +public: + /** + * \brief A single statement parameter, to be used in initializer lists. + * \details + * Applies lightweight type-erasure to types satisfying the `WritableField` concept, + * so they can be passed in initializer lists. + * + * \par Object lifetimes + * This is a view type, and should only be used in initializer lists, as a function argument. + */ + class statement_param_arg + { + field_view impl_; +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::access; +#endif + public: + /** + * \brief Constructor. + * \details + * Can be passed any type satisfying `WritableField`. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * No copies of `f` are performed. This type should only be used in initializer lists, to avoid + * lifetime issues. + */ + template < + BOOST_MYSQL_WRITABLE_FIELD WritableField +#ifndef BOOST_MYSQL_DOXYGEN + , + class = typename std::enable_if::value>::type +#endif + > + statement_param_arg(const WritableField& f) noexcept : impl_(detail::to_field(f)) + { + } + }; + + /** + * \brief Constructs a stage to execute a text query. + * \details + * Adding `*this` to a pipeline creates a stage that will run + * `query` as a SQL query, as per \ref any_connection::execute. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * No copies of `query` are performed. This type should only be used + * as a function argument to avoid lifetime issues. + */ + execute_stage(string_view query) noexcept : type_(type_query), data_(query) {} + + /** + * \brief Constructs a stage to execute a prepared statement. + * \details + * Adding `*this` to a pipeline creates a stage that will run + * `stmt` bound to any parameters passed in `params`, as per \ref any_connection::execute + * and \ref statement::bind. For example, `execute_stage(stmt, {42, "John"})` has + * effects equivalent to `conn.execute(stmt.bind(42, "John"))`. + * \n + * If your statement doesn't take any parameters, pass an empty initializer list + * as `params`. + * + * \par Exception safety + * No-throw guarantee. \n + * If the supplied number of parameters doesn't match the statement's number of parameters + * (i.e. `stmt.num_params() != params.size()`), a `std::invalid_argument` exception will + * be thrown when the stage is added to the pipeline (\ref pipeline_request::add or + * \ref static_pipeline_request::static_pipeline_request). + * + * \par Object lifetimes + * No copies of `params` or the values they point to are performed. + * This type should only be used as a function argument to avoid lifetime issues. + */ + execute_stage(statement stmt, std::initializer_list params) noexcept + : type_(type_stmt_tuple), data_(stmt, {params.begin(), params.size()}) + { + } + + /** + * \brief Constructs a stage to execute a prepared statement. + * \details + * Adding `*this` to a pipeline creates a stage that will run + * `stmt` bound to the parameter range passed in `params`, as per \ref any_connection::execute + * and \ref statement::bind. For example, `execute_stage(stmt, params_array)` has + * effects equivalent to `conn.execute(stmt.bind(params_array.begin(), params_array.end()))`. + * \n + * Use this signature when the number of parameters that your statement takes is not + * known at compile-time. + * + * \par Exception safety + * No-throw guarantee. \n + * If the supplied number of parameters doesn't match the statement's number of parameters + * (i.e. `stmt.num_params() != params.size()`), a `std::invalid_argument` exception will + * be raised when the stage is added to the pipeline (\ref pipeline_request::add or + * \ref static_pipeline_request::static_pipeline_request). + * + * \par Object lifetimes + * No copies of `params` or the values they point to are performed. + * This type should only be used as a function argument to avoid lifetime issues. + */ + execute_stage(statement stmt, span params) noexcept + : type_(type_stmt_range), data_(stmt, params) + { + } + + /** + * \brief The response type used by the static interface. + * \details + * A `boost::system::result` that contains a \ref results on success and + * a \ref errcode_with_diagnostics on failure. + */ + using response_type = system::result; + +private: + enum type_t + { + type_query, + type_stmt_tuple, + type_stmt_range + }; + + union data_t + { + string_view query; + struct + { + statement stmt; + span params; + } stmt_tuple; + struct + { + statement stmt; + span params; + } stmt_range; + + data_t(string_view q) noexcept : query(q) {} + data_t(statement s, span params) noexcept : stmt_tuple{s, params} {} + data_t(statement s, span params) noexcept : stmt_range{s, params} {} + }; + + type_t type_; + data_t data_; + + BOOST_MYSQL_DECL + detail::pipeline_request_stage create(std::vector& buffer) const; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::pipeline_stage_access; +#endif +}; + +/** + * \brief (EXPERIMENTAL) Pipeline stage type to prepare statements. + * \details + * To be used with \ref pipeline_request or \ref static_pipeline_request. + * Has effects equivalent to \ref any_connection::prepare_statement. + * + * \par Object lifetimes + * Like all stage types, this is a view type. Parameters required to run the stage + * are stored internally as views. Objects of this type should only be used as + * function or constructor parameters. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class prepare_statement_stage +{ + string_view stmt_sql_; + + BOOST_MYSQL_DECL + detail::pipeline_request_stage create(std::vector& buffer) const; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::pipeline_stage_access; +#endif + +public: + /** + * \brief Constructor. + * \details + * Adding a `prepare_statement_stage(stmt_sql)` to a pipeline + * creates a stage with effects equivalent to `conn.prepare_statement(stmt_sql)`. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * No copies of `stmt_sql` are performed. This type should only be used + * as a function argument to avoid lifetime issues. + */ + prepare_statement_stage(string_view stmt_sql) noexcept : stmt_sql_(stmt_sql) {} + + /** + * \brief The response type used by the static interface. + * \details + * A `boost::system::result` that contains a \ref statement on success and + * a \ref errcode_with_diagnostics on failure. + */ + using response_type = system::result; +}; + +/** + * \brief (EXPERIMENTAL) Pipeline stage type to close statements. + * \details + * To be used with \ref pipeline_request or \ref static_pipeline_request. + * Has effects equivalent to \ref any_connection::close_statement. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class close_statement_stage +{ + std::uint32_t stmt_id_; + + BOOST_MYSQL_DECL + detail::pipeline_request_stage create(std::vector& buffer) const; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::pipeline_stage_access; +#endif + +public: + /** + * \brief Constructor. + * \details + * Adding a `close_statement_stage(stmt)` to a pipeline + * creates a stage with effects equivalent to `conn.close_statement(stmt)`. + * + * \par Exception safety + * No-throw guarantee. + */ + close_statement_stage(statement stmt) noexcept : stmt_id_(stmt.id()) {} + + /// The response type used by the static interface. + using response_type = errcode_with_diagnostics; +}; + +/** + * \brief (EXPERIMENTAL) Pipeline stage type to reset session state. + * \details + * To be used with \ref pipeline_request or \ref static_pipeline_request. + * Has effects equivalent to \ref any_connection::reset_connection. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class reset_connection_stage +{ + BOOST_MYSQL_DECL + detail::pipeline_request_stage create(std::vector& buffer) const; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::pipeline_stage_access; +#endif + +public: + /** + * \brief Constructor. + * \details + * Adding a `reset_connection_stage()` to a pipeline + * creates a stage with effects equivalent to `conn.reset_connection()`. + * + * \par Exception safety + * No-throw guarantee. + */ + reset_connection_stage() = default; + + /// The response type used by the static interface. + using response_type = errcode_with_diagnostics; +}; + +/** + * \brief (EXPERIMENTAL) Pipeline stage type to set the connection's character set. + * \details + * To be used with \ref pipeline_request or \ref static_pipeline_request. + * Has effects equivalent to \ref any_connection::set_character_set. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class set_character_set_stage +{ + character_set charset_; + + BOOST_MYSQL_DECL + detail::pipeline_request_stage create(std::vector& buffer) const; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::pipeline_stage_access; +#endif + +public: + /** + * \brief Constructor. + * \details + * Adding `*this` to a pipeline creates a stage with effects + * equivalent to `conn.set_character_set(charset)`. + * + * \par Exception safety + * No-throw guarantee. \n + * If the supplied character set name is not valid (i.e. `charset.name` contains + * non-ASCII characters), a `std::invalid_argument` exception will + * be raised when the stage is added to the pipeline (\ref pipeline_request::add or + * \ref static_pipeline_request::static_pipeline_request). This never happens with + * the character sets provided by the library. The check is performed as a hardening measure. + */ + set_character_set_stage(character_set charset) noexcept : charset_(charset) {} + + /// The response type used by the static interface. + using response_type = errcode_with_diagnostics; +}; + +/** + * \brief (EXPERIMENTAL) A variant-like type holding the response of a single pipeline stage. + * \details + * When running dynamic pipelines with \ref pipeline_request, this type is used + * to hold individual stage responses. + * \n + * This is a variant-like type, similar to `boost::system::result`. At any point in time, + * it can contain: \n + * \li A \ref statement. Will happen if the stage was a prepare statement that succeeded. + * \li A \ref results. Will happen if the stage was a query or statement execution that succeeded. + * \li A \ref errcode_with_diagnostics. Will happen if the stage failed, or if it succeeded but + * it doesn't yield a value (as in close statement, reset connection and set character set). + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class any_stage_response +{ +#ifndef BOOST_MYSQL_DOXYGEN + struct + { + variant2::variant value; + + void emplace_results() { value.emplace(); } + void emplace_error() { value.emplace(); } + detail::execution_processor& get_processor() + { + return detail::access::get_impl(variant2::unsafe_get<2>(value)); + } + void set_result(statement s) { value = s; } + void set_error(error_code ec, diagnostics&& diag) + { + value.emplace<0>(errcode_with_diagnostics{ec, std::move(diag)}); + } + } impl_; + + friend struct detail::access; +#endif + + bool has_error() const { return impl_.value.index() == 0u; } + + BOOST_MYSQL_DECL + void check_has_results() const; + +public: + /** + * \brief Default constructor. + * \details + * Constructs an object containing an empty error (a default-constructed \ref errcode_with_diagnostics). + * + * \par Exception safety + * No-throw guarantee. + */ + any_stage_response() = default; + + /** + * \brief Returns true if the object contains a statement. + * + * \par Exception safety + * No-throw guarantee. + */ + bool has_statement() const noexcept { return impl_.value.index() == 1u; } + + /** + * \brief Returns true if the object contains a results. + * + * \par Exception safety + * No-throw guarantee. + */ + bool has_results() const noexcept { return impl_.value.index() == 2u; } + + /** + * \brief Retrieves the contained error (const lvalue reference accessor). + * \details + * If `*this` contains an error, retrieves it by copying it. + * Otherwise (if `this->has_statement() || this->has_results()`), + * returns an empty (default-constructed) error. + * + * \par Exception safety + * Strong guarantee. Memory allocations when copying the error's diagnostics may throw. + */ + errcode_with_diagnostics error() const& noexcept + { + return has_error() ? variant2::unsafe_get<0>(impl_.value) : errcode_with_diagnostics(); + } + + /** + * \brief Retrieves the contained error (rvalue reference accessor). + * \details + * If `*this` contains an error, retrieves it by moving it. + * Otherwise (if `this->has_statement() || this->has_results()`), + * returns an empty (default-constructed) error. + * + * \par Exception safety + * No-throw guarantee. + */ + errcode_with_diagnostics error() && noexcept + { + return has_error() ? variant2::unsafe_get<0>(std::move(impl_.value)) : errcode_with_diagnostics(); + } + + /** + * \brief Retrieves the contained statement or throws an exception. + * \details + * If `*this` contains an statement (`this->has_statement() == true`), + * retrieves it. Otherwise, throws an exception. + * + * \par Exception safety + * Strong guarantee. + * \throws std::invalid_argument If `*this` does not contain a statement. + */ + BOOST_MYSQL_DECL + statement as_statement() const; + + /** + * \brief Retrieves the contained statement (unchecked accessor). + * \details + * If `*this` contains an statement, + * retrieves it. Otherwise, the behavior is undefined. + * + * \par Preconditions + * `this->has_statement() == true` + * + * \par Exception safety + * No-throw guarantee. + */ + statement get_statement() const noexcept + { + BOOST_ASSERT(has_statement()); + return variant2::unsafe_get<1>(impl_.value); + } + + /** + * \brief Retrieves the contained results or throws an exception. + * \details + * If `*this` contains a `results` object (`this->has_results() == true`), + * retrieves a reference to it. Otherwise, throws an exception. + * + * \par Exception safety + * Strong guarantee. Throws on invalid input. + * \throws std::invalid_argument If `this->has_results() == false` + * + * \par Object lifetimes + * The returned reference is valid as long as `*this` is alive + * and hasn't been assigned to. + */ + const results& as_results() const& + { + check_has_results(); + return variant2::unsafe_get<2>(impl_.value); + } + + /// \copydoc as_results + results&& as_results() && + { + check_has_results(); + return variant2::unsafe_get<2>(std::move(impl_.value)); + } + + /** + * \brief Retrieves the contained results (unchecked accessor). + * \details + * If `*this` contains a `results` object, retrieves a reference to it. + * Otherwise, the behavior is undefined. + * + * \par Preconditions + * `this->has_results() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference is valid as long as `*this` is alive + * and hasn't been assigned to. + */ + const results& get_results() const& noexcept + { + BOOST_ASSERT(has_results()); + return variant2::unsafe_get<2>(impl_.value); + } + + /// \copydoc get_results + results&& get_results() && noexcept + { + BOOST_ASSERT(has_results()); + return variant2::unsafe_get<2>(std::move(impl_.value)); + } +}; + +/** + * \brief (EXPERIMENTAL) A dynamic pipeline request. + * \details + * Contains a collection of pipeline stages, fully describing the work to be performed + * by a pipeline operation. The number of stages and their type is determined at runtime. + * Call any of the `add_xxx` functions to append new stages to the request. + * \n + * If the number of stages and their type is known at compile time, prefer using + * \ref static_pipeline_request, instead. + * \n + * Stage responses are read into a vector of \ref any_stage_response, which is variant-like. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +class pipeline_request +{ +#ifndef BOOST_MYSQL_DOXYGEN + struct impl_t + { + std::vector buffer_; + std::vector stages_; + + detail::pipeline_request_view to_view() const { return {buffer_, stages_}; } + } impl_; + + friend struct detail::access; +#endif + +public: + /** + * \brief Default constructor. + * \details Constructs an empty pipeline request, with no stages. + * + * \par Exception safety + * No-throw guarantee. + */ + pipeline_request() = default; + + /** + * \brief Removes all stages in the pipeline request, making the object empty again. + * \details + * Can be used to re-use a single request object for multiple pipeline operations. + * + * \par Exception safety + * No-throw guarantee. + */ + void clear() noexcept + { + impl_.buffer_.clear(); + impl_.stages_.clear(); + } + + /** + * \brief Adds a stage to the pipeline request. + * \details + * Adds a stage with effects depending on the passed type. `PipelineStageType` + * should be one of: \n + * \li \ref execute_stage + * \li \ref prepare_statement_stage + * \li \ref close_statement_stage + * \li \ref reset_connection_stage + * \li \ref set_character_set_stage + * + * \par Exception safety + * Basic guarantee. Memory allocations while composing the request may throw. + * Throws if `stage` contains invalid parameters. Consult individual stage descriptions for details. + * \throws std::invalid_argument If `stage` contains invalid parameters. + * + * \par Object lifetimes + * All arguments are copied into the request buffer. Once this function returns, + * `stage` and any values that `stage` points to need not be kept alive. + */ + template + pipeline_request& add(PipelineStageType stage) + { + static_assert(detail::is_pipeline_stage_type::value, "Invalid PipelineStageType"); + impl_.stages_.push_back(detail::pipeline_stage_access::create(stage, impl_.buffer_)); + return *this; + } + + /** + * \brief The response type to use when running a pipeline with this request type, consisting of \ref + * any_stage_response objects. + */ + using response_type = std::vector; +}; + +/** + * \brief (EXPERIMENTAL) A static pipeline request type. + * \details + * Contains a collection of pipeline stages, fully describing the work to be performed + * by a pipeline operation. The pipeline contains as many stages as `PipelineStageType` + * template parameters are passed to this type. + * \n + * The following stage types may be used: \n + * \li \ref execute_stage + * \li \ref prepare_statement_stage + * \li \ref close_statement_stage + * \li \ref reset_connection_stage + * \li \ref set_character_set_stage + * \n + * Stage responses are read into `std::tuple` objects, with individual elements depending + * on stage types. See \ref response_type for more info. + * \n + * To create instances of this class without specifying template parameters, + * you can use \ref make_pipeline_request. If you're on C++17 or above, appropriate class deduction + * guides are also provided. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +template +class static_pipeline_request +{ + static_assert(sizeof...(PipelineStageType) > 0u, "A pipeline should have one stage, at least"); + static_assert( + mp11::mp_all_of, detail::is_pipeline_stage_type>::value, + "Some pipeline stage types are not valid" + ); + + struct impl_t + { + std::vector buffer_; + std::array stages_; + + detail::pipeline_request_view to_view() const { return {buffer_, stages_}; } + + impl_t(const PipelineStageType&... args) + : stages_{{detail::pipeline_stage_access::create(args, buffer_)...}} + { + } + } impl_; + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::access; +#endif + +public: + /** + * \brief Constructor. + * \details + * Takes as many arguments as stages the pipeline has. Stage types include + * all the parameters required to fully compose the request. + * + * \par Exception safety + * Strong guarantee. Memory allocations while composing the request may throw. + * Supplying steps with invalid arguments also throws. + * See individual stage descriptions for possible error conditions. + * \throws std::invalid_argument If any of `stages` contain invalid parameters. + * + * \par Object lifetimes + * Parameters are stored as a serialized request on the constructed object. + * The `stages` objects, and any other objects they point to, need not be kept alive + * once the constructor returns. + */ + static_pipeline_request(const PipelineStageType&... stages) : impl_(stages...) {} + + /** + * \brief The response type to use when running a pipeline with this request type. + * \details + * Has the same number of elements as pipeline stages. Consult each stage + * `response_type` typedef for more info. + */ + using response_type = std::tuple; +}; + +/** + * \brief (EXPERIMENTAL) Creates a static_pipeline_request object. + * \details + * This factory function can be used to avoid having to explicitly specify + * template parameters on the request type. For C++17 code and later, + * you can directly use \ref static_pipeline_request constructor, instead. + * + * \par Exception safety + * Strong guarantee. Memory allocations while composing the request may throw. + * Supplying steps with invalid arguments also throws. + * \throws std::invalid_argument If `stages` contains a stage with invalid parameters. + * See individual stage descriptions for possible error conditions. + * + * \par Object lifetimes + * The `stages` objects, and any other objects they point to, need not be kept alive + * once the constructor returns. + * + * \par Experimental + * This part of the API is experimental, and may change in successive + * releases without previous notice. + */ +template +static_pipeline_request make_pipeline_request(const PipelineStageType&... stages) +{ + return {stages...}; +} + +#ifndef BOOST_NO_CXX17_DEDUCTION_GUIDES +template +static_pipeline_request(const PipelineStageType&... args) -> static_pipeline_request; +#endif + +} // namespace mysql +} // namespace boost + +#include +#ifdef BOOST_MYSQL_HEADER_ONLY +#include +#endif + +#endif diff --git a/include/boost/mysql/src.hpp b/include/boost/mysql/src.hpp index ad5cb5be..2bf2464b 100644 --- a/include/boost/mysql/src.hpp +++ b/include/boost/mysql/src.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 75154c2a..9929edd0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources( INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/common/src/entry_point.cpp ${CMAKE_CURRENT_SOURCE_DIR}/common/src/tracker_executor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/common/src/printing.cpp ) target_link_libraries( boost_mysql_testing diff --git a/test/Jamfile b/test/Jamfile index f8c5b2c6..5b340e4d 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -113,6 +113,7 @@ alias common_test_sources : common/src/entry_point.cpp common/src/tracker_executor.cpp + common/src/printing.cpp ; # Boost.Context causes failures with warnings-as-errors diff --git a/test/common/include/test_common/printing.hpp b/test/common/include/test_common/printing.hpp index 5143d308..9ad3ac25 100644 --- a/test/common/include/test_common/printing.hpp +++ b/test/common/include/test_common/printing.hpp @@ -8,77 +8,41 @@ #ifndef BOOST_MYSQL_TEST_COMMON_INCLUDE_TEST_COMMON_PRINTING_HPP #define BOOST_MYSQL_TEST_COMMON_INCLUDE_TEST_COMMON_PRINTING_HPP -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include +#include namespace boost { namespace mysql { -inline std::ostream& operator<<(std::ostream& os, client_errc v) -{ - return os << get_client_category().message(static_cast(v)); -} +enum class client_errc; +std::ostream& operator<<(std::ostream& os, client_errc v); -inline std::ostream& operator<<(std::ostream& os, common_server_errc v) -{ - return os << get_common_server_category().message(static_cast(v)); -} +enum class common_server_errc; +std::ostream& operator<<(std::ostream& os, common_server_errc v); -inline std::ostream& operator<<(std::ostream& os, const diagnostics& diag) -{ - return os << diag.server_message(); -} +class diagnostics; +std::ostream& operator<<(std::ostream& os, const diagnostics& v); -inline std::ostream& operator<<(std::ostream& os, const row_view& value) -{ - os << '{'; - if (!value.empty()) - { - os << value[0]; - for (auto it = std::next(value.begin()); it != value.end(); ++it) - { - os << ", " << *it; - } - } - return os << '}'; -} +class row_view; +std::ostream& operator<<(std::ostream& os, const row_view& v); -inline std::ostream& operator<<(std::ostream& os, const row& r) { return os << row_view(r); } +class row; +std::ostream& operator<<(std::ostream& os, const row& v); -inline std::ostream& operator<<(std::ostream& os, metadata_mode v) -{ - switch (v) - { - case metadata_mode::full: return os << "full"; - case metadata_mode::minimal: return os << "minimal"; - default: return os << ""; - } -} +enum class metadata_mode; +std::ostream& operator<<(std::ostream& os, metadata_mode v); -inline std::ostream& operator<<(std::ostream& os, ssl_mode v) -{ - switch (v) - { - case ssl_mode::disable: return os << "disable"; - case ssl_mode::enable: return os << "enable"; - case ssl_mode::require: return os << "require"; - default: return os << ""; - } -} +enum class ssl_mode; +std::ostream& operator<<(std::ostream& os, ssl_mode v); + +struct character_set; +bool operator==(const character_set& lhs, const character_set& rhs); +std::ostream& operator<<(std::ostream& os, const character_set& v); + +struct errcode_with_diagnostics; +bool operator==(const errcode_with_diagnostics& lhs, const errcode_with_diagnostics& rhs); +std::ostream& operator<<(std::ostream& os, const errcode_with_diagnostics& v); } // namespace mysql } // namespace boost -BOOST_TEST_DONT_PRINT_LOG_VALUE(boost::mysql::time) - #endif diff --git a/test/common/src/printing.cpp b/test/common/src/printing.cpp new file mode 100644 index 00000000..706c10f6 --- /dev/null +++ b/test/common/src/printing.cpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2019-2024 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 +#include +#include +#include + +#include + +#include + +#include "test_common/printing.hpp" + +using namespace boost::mysql; + +std::ostream& boost::mysql::operator<<(std::ostream& os, client_errc v) { return os << error_code(v); } + +std::ostream& boost::mysql::operator<<(std::ostream& os, common_server_errc v) { return os << error_code(v); } + +std::ostream& boost::mysql::operator<<(std::ostream& os, const diagnostics& diag) +{ + const auto& impl = detail::access::get_impl(diag); + return os << "diagnostics{ " << (impl.is_server ? ".server_message" : ".client_message") << " = \"" + << impl.msg << "\" }"; +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, const row_view& value) +{ + os << '{'; + if (!value.empty()) + { + os << value[0]; + for (auto it = std::next(value.begin()); it != value.end(); ++it) + { + os << ", " << *it; + } + } + return os << '}'; +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, const row& r) { return os << row_view(r); } + +static const char* to_string(metadata_mode v) +{ + switch (v) + { + case metadata_mode::full: return "metadata_mode::full"; + case metadata_mode::minimal: return "metadata_mode::minimal"; + default: return ""; + } +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, metadata_mode v) { return os << ::to_string(v); } + +static const char* to_string(ssl_mode v) +{ + switch (v) + { + case ssl_mode::disable: return "ssl_mode::disable"; + case ssl_mode::enable: return "ssl_mode::enable"; + case ssl_mode::require: return "ssl_mode::require"; + default: return ""; + } +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, ssl_mode v) { return os << ::to_string(v); } + +// character set +bool boost::mysql::operator==(const character_set& lhs, const character_set& rhs) +{ + return lhs.name == rhs.name; +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, const character_set& v) +{ + return os << "character_set(\"" << v.name << "\")"; +} + +// errcode_with_diagnostics +bool boost::mysql::operator==(const errcode_with_diagnostics& lhs, const errcode_with_diagnostics& rhs) +{ + return lhs.code == rhs.code && lhs.diag == rhs.diag; +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, const errcode_with_diagnostics& v) +{ + return os << "errcode_with_diagnostics{ .code = " << v.code << ", .diag = " << v.diag << " }"; +} diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index fff7fc32..0189a697 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable( test/reconnect.cpp test/any_connection.cpp test/character_set_tracking.cpp + test/pipeline.cpp test/connection_pool.cpp test/db_specific.cpp test/database_types.cpp @@ -46,6 +47,7 @@ add_executable( test/snippets/any_connection.cpp test/snippets/connection_pool.cpp test/snippets/sql_formatting.cpp + test/snippets/pipeline.cpp ) target_include_directories( boost_mysql_integrationtests diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 3acbca87..08d46288 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -59,6 +59,7 @@ run test/reconnect.cpp test/any_connection.cpp test/character_set_tracking.cpp + test/pipeline.cpp test/connection_pool.cpp test/db_specific.cpp test/database_types.cpp @@ -76,6 +77,7 @@ run test/snippets/any_connection.cpp test/snippets/connection_pool.cpp test/snippets/sql_formatting.cpp + test/snippets/pipeline.cpp : requirements $(test_command) diff --git a/test/integration/include/test_integration/snippets/get_any_connection.hpp b/test/integration/include/test_integration/snippets/get_any_connection.hpp new file mode 100644 index 00000000..c9ebf5c3 --- /dev/null +++ b/test/integration/include/test_integration/snippets/get_any_connection.hpp @@ -0,0 +1,53 @@ +// +// Copyright (c) 2019-2024 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_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_GET_ANY_CONNECTION_HPP +#define BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_GET_ANY_CONNECTION_HPP + +#include +#include +#include + +#include + +#include "test_common/ci_server.hpp" +#include "test_integration/snippets/credentials.hpp" + +namespace boost { +namespace mysql { +namespace test { + +struct any_connection_and_context +{ + asio::io_context ctx; + any_connection conn{ctx.get_executor()}; + + any_connection_and_context() + { + // Connect + boost::mysql::connect_params params; + params.server_address.emplace_host_and_port(get_hostname()); + params.username = mysql_username; + params.password = mysql_password; + params.database = "boost_mysql_examples"; + params.ssl = ssl_mode::disable; + params.multi_queries = true; + conn.connect(params); + } +}; + +inline any_connection& get_any_connection() +{ + static any_connection_and_context obj; + return obj.conn; +} + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/integration/test/character_set_tracking.cpp b/test/integration/test/character_set_tracking.cpp index 6b25c1b9..a72ac5fd 100644 --- a/test/integration/test/character_set_tracking.cpp +++ b/test/integration/test/character_set_tracking.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/test/integration/test/pipeline.cpp b/test/integration/test/pipeline.cpp new file mode 100644 index 00000000..2775ea23 --- /dev/null +++ b/test/integration/test/pipeline.cpp @@ -0,0 +1,228 @@ +// +// Copyright (c) 2019-2024 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 +#include + +#include "test_common/create_basic.hpp" +#include "test_common/create_diagnostics.hpp" +#include "test_common/netfun_helpers.hpp" +#include "test_common/network_result.hpp" +#include "test_common/printing.hpp" +#include "test_integration/common.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; +namespace asio = boost::asio; +using boost::test_tools::per_element; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_pipeline) + +struct fixture +{ + asio::io_context ctx; + any_connection conn{ctx}; + + fixture() { conn.connect(default_connect_params(ssl_mode::disable)); } +}; + +BOOST_FIXTURE_TEST_CASE(dynamic_success, fixture) +{ + // Setup + pipeline_request req; + pipeline_request::response_type res; + + // Populate the request + req.add(reset_connection_stage()) + .add(execute_stage("SET @myvar = 15")) + .add(prepare_statement_stage("SELECT * FROM one_row_table WHERE id = ?")); + + // Run it + auto result = create_initial_netresult(); + conn.run_pipeline(req, res, result.err, *result.diag); + result.validate_no_error(); + + // Check results + BOOST_TEST_REQUIRE(res.size() == 3u); + BOOST_TEST(res[0].error() == errcode_with_diagnostics{}); + BOOST_TEST(!res[0].has_results()); + BOOST_TEST(!res[0].has_statement()); + BOOST_TEST(res[1].has_results()); + auto stmt = res[2].as_statement(); + BOOST_TEST(stmt.valid()); + BOOST_TEST(conn.current_character_set().error() == client_errc::unknown_character_set); + + // Re-populate the pipeline, with different stages + req.clear(); + req.add(set_character_set_stage(utf8mb4_charset)) + .add(execute_stage(stmt, {0})) + .add(execute_stage(stmt, {1})) + .add(close_statement_stage(stmt)); + + // Run it + result = create_initial_netresult(); + conn.run_pipeline(req, res, result.err, *result.diag); + result.validate_no_error(); + + // Check results + BOOST_TEST_REQUIRE(res.size() == 4u); + BOOST_TEST(res[0].error() == errcode_with_diagnostics{}); + BOOST_TEST(!res[0].has_results()); + BOOST_TEST(!res[0].has_statement()); + BOOST_TEST(res[1].as_results().rows() == rows(), per_element()); + BOOST_TEST(res[2].as_results().rows() == makerows(2, 1, "f0"), per_element()); + BOOST_TEST(res[3].error() == errcode_with_diagnostics{}); + BOOST_TEST(!res[3].has_results()); + BOOST_TEST(!res[3].has_statement()); +} + +BOOST_FIXTURE_TEST_CASE(dynamic_errors, fixture) +{ + // Setup + pipeline_request req; + pipeline_request::response_type res; + + // Populate the request with some successes and some errors + req.add(execute_stage("SET @myvar = 42")) // OK + .add(prepare_statement_stage("SELECT * FROM bad_table WHERE id = ?")) // error: bad table + .add(execute_stage("")) // error: empty query + .add(execute_stage("SELECT @myvar")) // OK + .add(set_character_set_stage(character_set{"bad_charset", utf8mb4_charset.next_char})) // bad charset + .add(execute_stage("SELECT 'abc'")); // OK + + // Run it. The result of the operation is the first encountered error + auto result = create_initial_netresult(); + conn.run_pipeline(req, res, result.err, *result.diag); + result.validate_error_exact( + common_server_errc::er_no_such_table, + "Table 'boost_mysql_integtests.bad_table' doesn't exist" + ); + + // Check results + BOOST_TEST_REQUIRE(res.size() == 6u); + BOOST_TEST(res[0].has_results()); + BOOST_TEST( + res[1].error() == (errcode_with_diagnostics{ + common_server_errc::er_no_such_table, + create_server_diag("Table 'boost_mysql_integtests.bad_table' doesn't exist") + }) + ); + BOOST_TEST( + res[2].error() == + (errcode_with_diagnostics{common_server_errc::er_empty_query, create_server_diag("Query was empty")}) + ); + BOOST_TEST(res[3].as_results().rows() == makerows(1, 42), per_element()); + BOOST_TEST( + res[4].error() == (errcode_with_diagnostics{ + common_server_errc::er_unknown_character_set, + create_server_diag("Unknown character set: 'bad_charset'") + }) + ); + BOOST_TEST(res[5].as_results().rows() == makerows(1, "abc"), per_element()); +} + +BOOST_FIXTURE_TEST_CASE(static_success, fixture) +{ + // Setup + auto req = make_pipeline_request( + reset_connection_stage(), + execute_stage("SET @myvar = 15"), + prepare_statement_stage("SELECT * FROM one_row_table WHERE id = ?") + ); + decltype(req)::response_type res; + + // Run it + auto result = create_initial_netresult(); + conn.run_pipeline(req, res, result.err, *result.diag); + result.validate_no_error(); + + // Check results + BOOST_TEST(std::get<0>(res) == errcode_with_diagnostics{}); + BOOST_TEST(std::get<1>(res).has_value()); + auto stmt = std::get<2>(res).value(); + BOOST_TEST(stmt.valid()); + BOOST_TEST(conn.current_character_set().error() == client_errc::unknown_character_set); + + // Create another pipeline + auto req2 = make_pipeline_request( + set_character_set_stage(utf8mb4_charset), + execute_stage(stmt, {0}), + execute_stage(stmt, {1}), + close_statement_stage(stmt) + ); + decltype(req2)::response_type res2; + + // Run it + result = create_initial_netresult(); + conn.run_pipeline(req2, res2, result.err, *result.diag); + result.validate_no_error(); + + // Check results + BOOST_TEST(std::get<0>(res2) == errcode_with_diagnostics{}); + BOOST_TEST(std::get<1>(res2).value().rows() == rows(), per_element()); + BOOST_TEST(std::get<2>(res2).value().rows() == makerows(2, 1, "f0"), per_element()); + BOOST_TEST(std::get<3>(res2) == errcode_with_diagnostics{}); +} + +BOOST_FIXTURE_TEST_CASE(static_errors, fixture) +{ + // Setup + auto req = make_pipeline_request( + execute_stage("SET @myvar = 42"), // OK + prepare_statement_stage("SELECT * FROM bad_table WHERE id = ?"), // error: bad table + execute_stage(""), // error: empty query + execute_stage("SELECT @myvar"), // OK + set_character_set_stage(character_set{"bad_charset", utf8mb4_charset.next_char}), // bad charset + execute_stage("SELECT 'abc'") // OK + ); + decltype(req)::response_type res; + + // Run it. The result of the operation is the first encountered error + auto result = create_initial_netresult(); + conn.run_pipeline(req, res, result.err, *result.diag); + result.validate_error_exact( + common_server_errc::er_no_such_table, + "Table 'boost_mysql_integtests.bad_table' doesn't exist" + ); + + // Check results + BOOST_TEST(std::get<0>(res).has_value()); + BOOST_TEST( + std::get<1>(res).error() == + (errcode_with_diagnostics{ + common_server_errc::er_no_such_table, + create_server_diag("Table 'boost_mysql_integtests.bad_table' doesn't exist") + }) + ); + BOOST_TEST( + std::get<2>(res).error() == + (errcode_with_diagnostics{common_server_errc::er_empty_query, create_server_diag("Query was empty")}) + ); + BOOST_TEST(std::get<3>(res).value().rows() == makerows(1, 42), per_element()); + BOOST_TEST( + std::get<4>(res) == (errcode_with_diagnostics{ + common_server_errc::er_unknown_character_set, + create_server_diag("Unknown character set: 'bad_charset'") + }) + ); + BOOST_TEST(std::get<5>(res).value().rows() == makerows(1, "abc"), per_element()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/integration/test/prepared_statements.cpp b/test/integration/test/prepared_statements.cpp index 30b84b4c..a371837b 100644 --- a/test/integration/test/prepared_statements.cpp +++ b/test/integration/test/prepared_statements.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 @@ -17,12 +18,9 @@ #include -#include - #include "test_common/create_basic.hpp" #include "test_common/printing.hpp" #include "test_integration/common.hpp" -#include "test_integration/er_connection.hpp" #include "test_integration/tcp_network_fixture.hpp" using namespace boost::mysql::test; diff --git a/test/integration/test/snippets/pipeline.cpp b/test/integration/test/snippets/pipeline.cpp new file mode 100644 index 00000000..f51abfac --- /dev/null +++ b/test/integration/test/snippets/pipeline.cpp @@ -0,0 +1,308 @@ +// +// Copyright (c) 2019-2024 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 + +#include +#include + +#include + +#include "test_common/create_basic.hpp" +#include "test_common/printing.hpp" +#include "test_integration/snippets/get_any_connection.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using boost::test_tools::per_element; + +namespace { + +BOOST_AUTO_TEST_CASE(section_pipeline_dynamic) +{ + auto& conn = get_any_connection(); + + //[pipeline_dynamic_request + // Create a pipeline request and add three stages to it. + // When run, this pipeline will set the connection's character set to utf8mb4 + // and prepare two statements. + pipeline_request req; + req.add(set_character_set_stage(utf8mb4_charset)) + .add(prepare_statement_stage("INSERT INTO audit_log (t, msg) VALUES (?, ?)")) + .add(prepare_statement_stage( + "INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)" + )); + //] + + //[pipeline_dynamic_run + // Run the pipeline request req, and store responses into res + // any_stage_response is a variant-like type that can store the response + // of any stage type (including results and statements). + std::vector res; + conn.run_pipeline(req, res); + //] + + //[pipeline_dynamic_results + // The 2nd and 3rd stages were statement preparation requests, + // so res[1] and res[2] contain statement objects + statement stmt1 = res[1].as_statement(); + statement stmt2 = res[2].as_statement(); + //] + + BOOST_TEST(stmt1.valid()); + BOOST_TEST(stmt2.valid()); +} + +BOOST_AUTO_TEST_CASE(section_pipeline_static) +{ + auto& conn = get_any_connection(); + + //[pipeline_static + // Create a pipeline request containing three stages. + // When run, this pipeline will set the connection's character set to utf8mb4 + // and prepare two statements. + // req is a static_pipeline_request + auto req = make_pipeline_request( + set_character_set_stage(utf8mb4_charset), + prepare_statement_stage("INSERT INTO audit_log (t, msg) VALUES (?, ?)"), + prepare_statement_stage("INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)") + ); + + // Create a response object. Instead of vectors, static pipelines use tuples. + // Each stage type has an associated response type + // res is a std::tuple + decltype(req)::response_type res; + + // Run the pipeline + conn.run_pipeline(req, res); + + // Access your statements + statement stmt1 = std::get<1>(res).value(); + statement stmt2 = std::get<2>(res).value(); + //] + + BOOST_TEST(stmt1.valid()); + BOOST_TEST(stmt2.valid()); +} + +BOOST_AUTO_TEST_CASE(section_pipeline_errors) +{ + auto& conn = get_any_connection(); + + //[pipeline_errors + // The second step in the pipeline will fail, the other ones will succeeded + pipeline_request req; + req.add(set_character_set_stage(utf8mb4_charset)) + .add(prepare_statement_stage("INSERT INTO bad_table (t, msg) VALUES (?, ?)")) // will fail + .add(prepare_statement_stage( + "INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)" + )); + + std::vector res; + error_code ec; + diagnostics diag; + + conn.run_pipeline(req, res, ec, diag); + BOOST_TEST(ec == common_server_errc::er_no_such_table); + BOOST_TEST(res[0].error().code == error_code()); + BOOST_TEST(res[1].error().code == common_server_errc::er_no_such_table); + BOOST_TEST(res[2].error().code == error_code()); + //] +} + +BOOST_AUTO_TEST_CASE(section_pipeline_pitfalls) +{ + auto& conn = get_any_connection(); + + { + //[pipeline_pitfalls_bad + // This doesn't behave correctly - DO NOT DO THIS + // The first INSERT will fail due to a failed foreign key check (there is no such company), + // but COMMIT will still be run, thus leaving us with an inconsistent data model + pipeline_request req; + + req.add(execute_stage("START TRANSACTION")) + .add(execute_stage( + "INSERT INTO employee (first_name, last_name, company_id) VALUES ('John', 'Doe', 'bad')" + )) + .add(execute_stage("INSERT INTO logs VALUES ('Inserted 1 employee')")) + .add(execute_stage("COMMIT")); + //] + + decltype(req)::response_type res; + error_code ec; + diagnostics diag; + conn.run_pipeline(req, res, ec, diag); + BOOST_TEST(ec == common_server_errc::er_no_referenced_row_2); + } + + { + try + { + //[pipeline_pitfalls_good + const char* sql = + "START TRANSACTION;" + "INSERT INTO employee (first_name, last_name, company_id) VALUES ('John', 'Doe', 'bad');" + "INSERT INTO logs VALUES ('Inserted 1 employee');" + "COMMIT"; + + // After the first INSERT fails, nothing else will be run. This is what we want. + // Note that you need to enable multi queries when connecting to be able to run this. + results r; + conn.execute(sql, r); + //] + } + catch (const boost::system::system_error& err) + { + BOOST_TEST(err.code() == common_server_errc::er_no_referenced_row_2); + } + } +} + +BOOST_AUTO_TEST_CASE(section_pipeline_reference) +{ + auto& conn = get_any_connection(); + results result; + std::vector pipe_res; + + // Execute + { + pipeline_request req; + auto stmt = conn.prepare_statement("SELECT ?, ?, ?"); + + //[pipeline_reference_execute + // Text query + req.add(execute_stage("SELECT 1")); + + // Prepared statement, with number of parameters known at compile time + req.add(execute_stage(stmt, {"John", "Doe", 42})); + + // Prepared statement, with number of parameters unknown at compile time + std::vector params{ + /* ... */ + //<- + field_view("Janet"), + field_view("Joyce"), + field_view(50) + //-> + }; + req.add(execute_stage(stmt, params)); + //] + + conn.run_pipeline(req, pipe_res); + BOOST_TEST(pipe_res.at(0).as_results().rows() == makerows(1, 1), per_element()); + BOOST_TEST(pipe_res.at(1).as_results().rows() == makerows(3, "John", "Doe", 42), per_element()); + BOOST_TEST(pipe_res.at(2).as_results().rows() == makerows(3, "Janet", "Joyce", 50), per_element()); + } + { + auto stmt = conn.prepare_statement("SELECT ?, ?, ?"); + + //[pipeline_reference_execute_equivalent + // Text query + conn.execute("SELECT 1", result); + //<- + BOOST_TEST(result.rows() == makerows(1, 1), per_element()); + //-> + + // Prepared statement, with number of parameters known at compile time + conn.execute(stmt.bind("John", "Doe", 42), result); + //<- + BOOST_TEST(result.rows() == makerows(3, "John", "Doe", 42), per_element()); + //-> + + // Prepared statement, with number of parameters unknown at compile time + std::vector params{ + /* ... */ + //<- + field_view("Janet"), + field_view("Joyce"), + field_view(50) + //-> + }; + conn.execute(stmt.bind(params.begin(), params.end()), result); + //] + + BOOST_TEST(result.rows() == makerows(3, "Janet", "Joyce", 50), per_element()); + } + + // Prepare statement + { + pipeline_request req; + + //[pipeline_reference_prepare_statement + req.add(prepare_statement_stage("SELECT * FROM employee WHERE id = ?")); + //] + + conn.run_pipeline(req, pipe_res); + BOOST_TEST(pipe_res.at(0).as_statement().valid()); + } + { + //[pipeline_reference_prepare_statement_equivalent + statement stmt = conn.prepare_statement("SELECT * FROM employee WHERE id = ?"); + //] + + BOOST_TEST(stmt.valid()); + } + + // Close statement + { + pipeline_request req; + auto stmt = conn.prepare_statement("SELECT 1"); + + //[pipeline_reference_close_statement + req.add(close_statement_stage(stmt)); + //] + + conn.run_pipeline(req, pipe_res); + } + { + auto stmt = conn.prepare_statement("SELECT 1"); + + //[pipeline_reference_close_statement_equivalent + conn.close_statement(stmt); + //] + } + + // Reset connection + { + pipeline_request req; + + //[pipeline_reference_reset_connection + req.add(reset_connection_stage()); + //] + } + { + //[pipeline_reference_reset_connection_equivalent + conn.reset_connection(); + //] + } + + // Set character set + { + pipeline_request req; + //[pipeline_reference_set_character_set + req.add(set_character_set_stage(utf8mb4_charset)); + //] + } + { + //[pipeline_reference_set_character_set_equivalent + conn.set_character_set(utf8mb4_charset); + //] + } +} + +} // namespace diff --git a/test/integration/test/snippets/sql_formatting.cpp b/test/integration/test/snippets/sql_formatting.cpp index 00a76673..35c2eadf 100644 --- a/test/integration/test/snippets/sql_formatting.cpp +++ b/test/integration/test/snippets/sql_formatting.cpp @@ -25,9 +25,8 @@ #include #include -#include "test_common/ci_server.hpp" #include "test_common/printing.hpp" -#include "test_integration/snippets/credentials.hpp" +#include "test_integration/snippets/get_any_connection.hpp" #ifndef BOOST_NO_CXX17_HDR_OPTIONAL #include @@ -157,22 +156,9 @@ void test_compose_update_query() BOOST_AUTO_TEST_CASE(section_sql_formatting) { - auto server_hostname = get_hostname(); - - connect_params params; - params.server_address.emplace_host_and_port(server_hostname); - params.username = mysql_username; - params.password = mysql_password; - params.database = "boost_mysql_examples"; - params.multi_queries = true; - params.ssl = ssl_mode::disable; - - boost::asio::io_context ioc; - any_connection conn(ioc); + auto& conn = get_any_connection(); results r; - conn.connect(params); - { //[sql_formatting_simple std::string employee_name = get_name(); // employee_name is an untrusted string diff --git a/test/integration/test/spotchecks.cpp b/test/integration/test/spotchecks.cpp index ee554329..20e0c71f 100644 --- a/test/integration/test/spotchecks.cpp +++ b/test/integration/test/spotchecks.cpp @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -20,7 +22,9 @@ #include #include "test_common/create_basic.hpp" +#include "test_common/create_diagnostics.hpp" #include "test_common/netfun_maker.hpp" +#include "test_common/printing.hpp" #include "test_integration/common.hpp" #include "test_integration/er_connection.hpp" #include "test_integration/network_test.hpp" @@ -496,7 +500,7 @@ struct }, }; -BOOST_AUTO_TEST_CASE(spotcheck_success) +BOOST_AUTO_TEST_CASE(set_character_set_success) { for (const auto& fns : set_charset_all_fns) { @@ -516,7 +520,7 @@ BOOST_AUTO_TEST_CASE(spotcheck_success) } } -BOOST_AUTO_TEST_CASE(spotcheck_error) +BOOST_AUTO_TEST_CASE(set_character_set_error) { for (const auto& fns : set_charset_all_fns) { @@ -540,4 +544,89 @@ BOOST_AUTO_TEST_CASE(spotcheck_error) } } +// same for run_pipeline +using run_pipeline_netmaker = netfun_maker_mem< + void, + any_connection, + const pipeline_request&, + pipeline_request::response_type&>; + +struct +{ + string_view name; + run_pipeline_netmaker::signature run_pipeline; +} run_pipeline_all_fns[] = { + {"sync_errc", run_pipeline_netmaker::sync_errc(&any_connection::run_pipeline)}, + {"sync_exc", run_pipeline_netmaker::sync_exc(&any_connection::run_pipeline)}, + {"async_errinfo", run_pipeline_netmaker::async_errinfo(&any_connection::async_run_pipeline, false)}, + {"async_noerrinfo", run_pipeline_netmaker::async_noerrinfo(&any_connection::async_run_pipeline, false)}, +}; + +BOOST_AUTO_TEST_CASE(run_pipeline_success) +{ + for (const auto& fns : run_pipeline_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)); + pipeline_request req; + req.add(set_character_set_stage(ascii_charset)) + .add(execute_stage("SET @myvar = 42")) + .add(execute_stage("SELECT @myvar")); + pipeline_request::response_type res; + + // Issue the pipeline + fns.run_pipeline(conn, req, res).validate_no_error(); + + // Success + BOOST_TEST(conn.current_character_set().value() == ascii_charset); + BOOST_TEST_REQUIRE(res.size() == 3u); + BOOST_TEST(res.at(0).error() == errcode_with_diagnostics()); + BOOST_TEST(res.at(1).as_results().rows().empty()); + BOOST_TEST(res.at(2).as_results().rows() == makerows(1, 42)); + } + } +} + +BOOST_AUTO_TEST_CASE(run_pipeline_error) +{ + for (const auto& fns : run_pipeline_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)); + pipeline_request req; + req.add(execute_stage("SET @myvar = 42")) + .add(prepare_statement_stage("SELECT * FROM bad_table")) + .add(execute_stage("SELECT @myvar")); + pipeline_request::response_type res; + + // Issue the command + fns.run_pipeline(conn, req, res) + .validate_error_exact( + common_server_errc::er_no_such_table, + "Table 'boost_mysql_integtests.bad_table' doesn't exist" + ); + + // Stages 0 and 2 were executed successfully + BOOST_TEST(res.size() == 3u); + BOOST_TEST(res[0].as_results().rows().size() == 0u); + BOOST_TEST( + res[1].error() == + (errcode_with_diagnostics{ + common_server_errc::er_no_such_table, + create_server_diag("Table 'boost_mysql_integtests.bad_table' doesn't exist") + }) + ); + BOOST_TEST(res[2].as_results().rows() == makerows(1, 42)); + } + } +} + BOOST_AUTO_TEST_SUITE_END() // test_spotchecks diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index ce5a2ee5..87c0d7a2 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable( # Helpers src/test_stream.cpp + src/printing.cpp # Actual tests test/auth/auth.cpp @@ -28,7 +29,6 @@ add_executable( test/protocol/deserialization.cpp test/sansio/read_buffer.cpp - test/sansio/message_writer.cpp test/sansio/message_reader.cpp test/sansio/top_level_algo.cpp @@ -41,6 +41,8 @@ add_executable( test/sansio/set_character_set.cpp test/sansio/ping.cpp test/sansio/reset_connection.cpp + test/sansio/prepare_statement.cpp + test/sansio/run_pipeline.cpp test/execution_processor/execution_processor.cpp test/execution_processor/execution_state_impl.cpp @@ -116,6 +118,7 @@ add_executable( test/escape_string.cpp test/constant_string_view.cpp test/pfr.cpp + test/pipeline.cpp ) target_include_directories( boost_mysql_unittests diff --git a/test/unit/Jamfile b/test/unit/Jamfile index 25dd2f7b..9546de66 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -19,6 +19,7 @@ run # Helpers src/test_stream.cpp + src/printing.cpp # Actual tests test/auth/auth.cpp @@ -37,7 +38,6 @@ run test/protocol/deserialization.cpp test/sansio/read_buffer.cpp - test/sansio/message_writer.cpp test/sansio/message_reader.cpp test/sansio/top_level_algo.cpp @@ -50,6 +50,8 @@ run test/sansio/set_character_set.cpp test/sansio/ping.cpp test/sansio/reset_connection.cpp + test/sansio/prepare_statement.cpp + test/sansio/run_pipeline.cpp test/execution_processor/execution_processor.cpp test/execution_processor/execution_state_impl.cpp @@ -125,6 +127,7 @@ run test/escape_string.cpp test/constant_string_view.cpp test/pfr.cpp + test/pipeline.cpp : requirements include diff --git a/test/unit/include/test_unit/algo_test.hpp b/test/unit/include/test_unit/algo_test.hpp index 25757994..07ce4cff 100644 --- a/test/unit/include/test_unit/algo_test.hpp +++ b/test/unit/include/test_unit/algo_test.hpp @@ -14,10 +14,10 @@ #include #include -#include #include #include +#include #include #include @@ -38,41 +38,40 @@ namespace test { class any_algo_ref { template - static detail::next_action do_resume(detail::sansio_algorithm* self, error_code ec) + static detail::next_action do_resume(void* self, detail::connection_state_data& st, error_code ec) { - return static_cast(self)->resume(ec); + return static_cast(self)->resume(st, ec); } - using fn_t = detail::next_action (*)(detail::sansio_algorithm*, error_code); + using fn_t = detail::next_action (*)(void*, detail::connection_state_data&, error_code); - detail::sansio_algorithm* algo_{}; + void* algo_{}; fn_t fn_{}; public: - template < - class Algo, - class = typename std::enable_if::value>::type> - any_algo_ref(Algo& op) noexcept : algo_(&op), fn_(&do_resume) + template ::value>::type> + any_algo_ref(Algo& algo) noexcept : algo_(&algo), fn_(&do_resume) { } - detail::sansio_algorithm& get() noexcept { return *algo_; } - const detail::sansio_algorithm& get() const noexcept { return *algo_; } - detail::next_action resume(error_code ec) { return fn_(algo_, ec); } + detail::next_action resume(detail::connection_state_data& st, error_code ec) + { + return fn_(algo_, st, ec); + } }; class BOOST_ATTRIBUTE_NODISCARD algo_test { struct step_t { - detail::next_action::type_t type; + detail::next_action_type type; std::vector bytes; error_code result; }; std::vector steps_; - void handle_read(const step_t& op, detail::connection_state_data& st) + static void handle_read(detail::connection_state_data& st, const step_t& op) { if (!op.result) { @@ -91,24 +90,27 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test } } - void handle_write(const step_t& op, detail::connection_state_data& st) + static void handle_write(span actual_msg, const step_t& op) { - // Multi-frame messages are not supported by these tests (they don't add anything) - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st.writer.current_chunk(), op.bytes); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(actual_msg, op.bytes); } - algo_test& add_step(detail::next_action::type_t act_type, std::vector bytes, error_code ec) + algo_test& add_step(detail::next_action_type act_type, std::vector bytes, error_code ec) { steps_.push_back(step_t{act_type, std::move(bytes), ec}); return *this; } - detail::next_action run_algo_until_step(any_algo_ref algo, std::size_t num_steps_to_run) + detail::next_action run_algo_until_step( + detail::connection_state_data& st, + any_algo_ref algo, + std::size_t num_steps_to_run + ) const { assert(num_steps_to_run <= num_steps()); // Start the op - auto act = algo.resume(error_code()); + auto act = algo.resume(st, error_code()); // Go through the requested steps for (std::size_t i = 0; i < num_steps_to_run; ++i) @@ -117,44 +119,48 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test { const auto& step = steps_[i]; BOOST_TEST_REQUIRE(act.type() == step.type); - if (step.type == detail::next_action::type_t::read) - handle_read(step, algo.get().conn_state()); - else if (step.type == detail::next_action::type_t::write) - handle_write(step, algo.get().conn_state()); + if (step.type == detail::next_action_type::read) + handle_read(st, step); + else if (step.type == detail::next_action_type::write) + handle_write(act.write_args().buffer, step); // Other actions don't need any handling - act = algo.resume(step.result); + act = algo.resume(st, step.result); } } return act; } - std::size_t num_steps() const noexcept { return steps_.size(); } + std::size_t num_steps() const { return steps_.size(); } - void check_impl(any_algo_ref algo, error_code expected_ec = {}) + void check_impl(detail::connection_state_data& st, any_algo_ref algo, error_code expected_ec = {}) const { // Run the op until completion - auto act = run_algo_until_step(algo, steps_.size()); + auto act = run_algo_until_step(st, algo, steps_.size()); // Check that we've finished - BOOST_TEST_REQUIRE(act.type() == detail::next_action::type_t::none); + BOOST_TEST_REQUIRE(act.type() == detail::next_action_type::none); BOOST_TEST(act.error() == expected_ec); } - void check_network_errors_impl(any_algo_ref algo, std::size_t step_number) + void check_network_errors_impl( + detail::connection_state_data& st, + any_algo_ref algo, + std::size_t step_number + ) const { assert(step_number < num_steps()); // Run all the steps that shouldn't cause an error - auto act = run_algo_until_step(algo, step_number); + auto act = run_algo_until_step(st, algo, step_number); BOOST_TEST_REQUIRE(act.type() == steps_[step_number].type); // Trigger an error in the requested step - act = algo.resume(asio::error::bad_descriptor); + act = algo.resume(st, asio::error::bad_descriptor); // The operation finished and returned the network error - BOOST_TEST_REQUIRE(act.type() == detail::next_action::type_t::none); + BOOST_TEST_REQUIRE(act.type() == detail::next_action_type::none); BOOST_TEST(act.error() == error_code(asio::error::bad_descriptor)); } @@ -164,55 +170,52 @@ public: BOOST_ATTRIBUTE_NODISCARD algo_test& expect_write(std::vector bytes, error_code result = {}) { - return add_step(detail::next_action::type_t::write, std::move(bytes), result); + return add_step(detail::next_action_type::write, std::move(bytes), result); } BOOST_ATTRIBUTE_NODISCARD algo_test& expect_read(std::vector result_bytes) { - return add_step(detail::next_action::type_t::read, std::move(result_bytes), error_code()); + return add_step(detail::next_action_type::read, std::move(result_bytes), error_code()); } BOOST_ATTRIBUTE_NODISCARD - algo_test& expect_read(error_code result) - { - return add_step(detail::next_action::type_t::read, {}, result); - } + algo_test& expect_read(error_code result) { return add_step(detail::next_action_type::read, {}, result); } BOOST_ATTRIBUTE_NODISCARD algo_test& expect_ssl_handshake(error_code result = {}) { - return add_step(detail::next_action::type_t::ssl_handshake, {}, result); + return add_step(detail::next_action_type::ssl_handshake, {}, result); } BOOST_ATTRIBUTE_NODISCARD algo_test& expect_ssl_shutdown(error_code result = {}) { - return add_step(detail::next_action::type_t::ssl_shutdown, {}, result); + return add_step(detail::next_action_type::ssl_shutdown, {}, result); } BOOST_ATTRIBUTE_NODISCARD algo_test& expect_close(error_code result = {}) { - return add_step(detail::next_action::type_t::close, {}, result); + return add_step(detail::next_action_type::close, {}, result); } template - void check(AlgoFixture& fix, error_code expected_ec = {}, const diagnostics& expected_diag = {}) + void check(AlgoFixture& fix, error_code expected_ec = {}, const diagnostics& expected_diag = {}) const { - check_impl(fix.algo, expected_ec); + check_impl(fix.st, fix.algo, expected_ec); BOOST_TEST(fix.diag == expected_diag); } template - void check_network_errors() + void check_network_errors() const { for (std::size_t i = 0; i < num_steps(); ++i) { BOOST_TEST_CONTEXT("check_network_errors erroring at step " << i) { AlgoFixture fix; - check_network_errors_impl(fix.algo, i); + check_network_errors_impl(fix.st, fix.algo, i); BOOST_TEST(fix.diag == diagnostics()); } } @@ -222,7 +225,13 @@ public: struct algo_fixture_base { detail::connection_state_data st{512}; - diagnostics diag{create_server_diag("Diagnostics not cleared")}; + diagnostics diag; + + algo_fixture_base(diagnostics initial_diag = create_server_diag("Diagnostics not cleared")) + : diag(std::move(initial_diag)) + { + st.write_buffer.push_back(0xff); // Check that we clear the write buffer at each step + } }; } // namespace test diff --git a/test/unit/include/test_unit/create_prepare_statement_response.hpp b/test/unit/include/test_unit/create_prepare_statement_response.hpp new file mode 100644 index 00000000..11d5c857 --- /dev/null +++ b/test/unit/include/test_unit/create_prepare_statement_response.hpp @@ -0,0 +1,77 @@ +// +// Copyright (c) 2019-2024 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_TEST_UNIT_INCLUDE_TEST_UNIT_CREATE_PREPARE_STATEMENT_RESPONSE_HPP +#define BOOST_MYSQL_TEST_UNIT_INCLUDE_TEST_UNIT_CREATE_PREPARE_STATEMENT_RESPONSE_HPP + +#include +#include + +#include +#include + +#include "test_unit/create_frame.hpp" + +namespace boost { +namespace mysql { +namespace test { + +class prepare_stmt_response_builder +{ + std::uint8_t seqnum_{}; + std::uint32_t statement_id_{0}; + std::uint16_t num_columns_{0}; + std::uint16_t num_params_{0}; + +public: + prepare_stmt_response_builder() = default; + + prepare_stmt_response_builder& seqnum(std::uint8_t v) + { + seqnum_ = v; + return *this; + } + + prepare_stmt_response_builder& id(std::uint32_t v) + { + statement_id_ = v; + return *this; + } + + prepare_stmt_response_builder& num_columns(std::uint16_t v) + { + num_columns_ = v; + return *this; + } + + prepare_stmt_response_builder& num_params(std::uint16_t v) + { + num_params_ = v; + return *this; + } + + std::vector build() const + { + std::vector res; + detail::serialization_context ctx(res, detail::disable_framing); + ctx.serialize( + detail::int1{0u}, // OK header + detail::int4{statement_id_}, // statement_id + detail::int2{num_columns_}, // num columns + detail::int2{num_params_}, // num_params + detail::int1{0u}, // reserved + detail::int2{90u} // warning_count + ); + return create_frame(seqnum_, res); + } +}; + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/unit/include/test_unit/create_query_frame.hpp b/test/unit/include/test_unit/create_query_frame.hpp index 425024f8..23cf488d 100644 --- a/test/unit/include/test_unit/create_query_frame.hpp +++ b/test/unit/include/test_unit/create_query_frame.hpp @@ -22,18 +22,23 @@ namespace boost { namespace mysql { namespace test { -inline std::vector create_query_body(string_view sql) +inline std::vector create_query_body_impl(std::uint8_t command_id, string_view sql) { std::vector buff; detail::serialization_context ctx(buff, detail::disable_framing); - ctx.add(0x03); + ctx.add(command_id); ctx.add(detail::to_span(sql)); return buff; } inline std::vector create_query_frame(std::uint8_t seqnum, string_view sql) { - return create_frame(seqnum, create_query_body(sql)); + return create_frame(seqnum, create_query_body_impl(0x03, sql)); +} + +inline std::vector create_prepare_statement_frame(std::uint8_t seqnum, string_view sql) +{ + return create_frame(seqnum, create_query_body_impl(0x16, sql)); } } // namespace test diff --git a/test/unit/include/test_unit/fail_count.hpp b/test/unit/include/test_unit/fail_count.hpp index 1d406de5..3360c192 100644 --- a/test/unit/include/test_unit/fail_count.hpp +++ b/test/unit/include/test_unit/fail_count.hpp @@ -30,7 +30,7 @@ public: : fail_after_(fail_after), err_(err) { } - error_code maybe_fail() noexcept { return ++num_calls_ >= fail_after_ ? err_ : error_code(); } + error_code maybe_fail() noexcept { return num_calls_++ >= fail_after_ ? err_ : error_code(); } }; } // namespace test diff --git a/test/unit/include/test_unit/pool_printing.hpp b/test/unit/include/test_unit/pool_printing.hpp deleted file mode 100644 index 769c5d90..00000000 --- a/test/unit/include/test_unit/pool_printing.hpp +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright (c) 2019-2024 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_TEST_UNIT_INCLUDE_TEST_UNIT_POOL_PRINTING_HPP -#define BOOST_MYSQL_TEST_UNIT_INCLUDE_TEST_UNIT_POOL_PRINTING_HPP - -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -inline std::ostream& operator<<(std::ostream& os, connection_status v) -{ - switch (v) - { - case connection_status::initial: return os << "connection_status::initial"; - case connection_status::connect_in_progress: return os << "connection_status::connect_in_progress"; - case connection_status::sleep_connect_failed_in_progress: - return os << "connection_status::sleep_connect_failed_in_progress"; - case connection_status::reset_in_progress: return os << "connection_status::reset_in_progress"; - case connection_status::ping_in_progress: return os << "connection_status::ping_in_progress"; - case connection_status::idle: return os << "connection_status::idle"; - case connection_status::in_use: return os << "connection_status::in_use"; - default: return os << ""; - } -} - -inline std::ostream& operator<<(std::ostream& os, collection_state v) -{ - switch (v) - { - case collection_state::needs_collect: return os << "collection_state::needs_collect"; - case collection_state::needs_collect_with_reset: - return os << "collection_state::needs_collect_with_reset"; - case collection_state::none: return os << "collection_state::none"; - default: return os << ""; - } -} - -inline std::ostream& operator<<(std::ostream& os, next_connection_action v) -{ - switch (v) - { - case next_connection_action::none: return os << "next_connection_action::none"; - case next_connection_action::connect: return os << "next_connection_action::connect"; - case next_connection_action::sleep_connect_failed: - return os << "next_connection_action::sleep_connect_failed"; - case next_connection_action::idle_wait: return os << "next_connection_action::idle_wait"; - case next_connection_action::reset: return os << "next_connection_action::reset"; - case next_connection_action::ping: return os << "next_connection_action::ping"; - default: return os << ""; - } -} - -} // namespace detail -} // namespace mysql -} // namespace boost - -#endif diff --git a/test/unit/include/test_unit/printing.hpp b/test/unit/include/test_unit/printing.hpp index cae18f59..8aab69c6 100644 --- a/test/unit/include/test_unit/printing.hpp +++ b/test/unit/include/test_unit/printing.hpp @@ -8,77 +8,57 @@ #ifndef BOOST_MYSQL_TEST_UNIT_INCLUDE_TEST_UNIT_PRINTING_HPP #define BOOST_MYSQL_TEST_UNIT_INCLUDE_TEST_UNIT_PRINTING_HPP -#include - -#include -#include -#include -#include - -#include -#include - -#include - -#include +#include namespace boost { namespace mysql { -inline std::ostream& operator<<(std::ostream& os, address_type value) -{ - switch (value) - { - case address_type::host_and_port: return os << "address_type::host_and_port"; - case address_type::unix_path: return os << "address_type::unix_path"; - default: return os << ""; - } -} +// address_type +enum class address_type; +std::ostream& operator<<(std::ostream& os, address_type value); namespace detail { -inline std::ostream& operator<<(std::ostream& os, capabilities caps) -{ - return os << "capabilities(" << caps.get() << ")"; -} +// capabilities +class capabilities; +std::ostream& operator<<(std::ostream& os, const capabilities& caps); -inline std::ostream& operator<<(std::ostream& os, db_flavor value) -{ - switch (value) - { - case db_flavor::mysql: return os << "mysql"; - case db_flavor::mariadb: return os << "mariadb"; - default: return os << ""; - } -} +// db_flavor +enum class db_flavor; +std::ostream& operator<<(std::ostream& os, db_flavor value); -inline std::ostream& operator<<(std::ostream& os, resultset_encoding t) -{ - switch (t) - { - case resultset_encoding::binary: return os << "binary"; - case resultset_encoding::text: return os << "text"; - default: return os << "unknown"; - } -} +// resultset_encoding +enum class resultset_encoding; +std::ostream& operator<<(std::ostream& os, resultset_encoding t); -inline std::ostream& operator<<(std::ostream& os, results_iterator it) -{ - return os << "results_iterator(" << static_cast(it.obj()) << ", index=" << it.index() << ")"; -} +// results_iterator +class results_iterator; +std::ostream& operator<<(std::ostream& os, const results_iterator& it); -inline std::ostream& operator<<(std::ostream& os, next_action::type_t t) -{ - switch (t) - { - case next_action::type_t::none: return os << "next_action::type_t::none"; - case next_action::type_t::read: return os << "next_action::type_t::read"; - case next_action::type_t::write: return os << "next_action::type_t::write"; - case next_action::type_t::ssl_handshake: return os << "next_action::type_t::ssl_handshake"; - case next_action::type_t::ssl_shutdown: return os << "next_action::type_t::ssh_shutdown"; - default: return os << ""; - } -} +// next_action_type +enum class next_action_type; +std::ostream& operator<<(std::ostream& os, next_action_type t); + +// pipeline_stage_kind +enum class pipeline_stage_kind; +std::ostream& operator<<(std::ostream& os, pipeline_stage_kind v); + +// pipeline_request_stage +struct pipeline_request_stage; +bool operator==(const pipeline_request_stage& lhs, const pipeline_request_stage& rhs); +std::ostream& operator<<(std::ostream& os, pipeline_request_stage v); + +// connection_status (pool) +enum class connection_status; +std::ostream& operator<<(std::ostream& os, connection_status v); + +// collection_state (pool) +enum class collection_state; +std::ostream& operator<<(std::ostream& os, collection_state v); + +// next_connection_action (pool) +enum class next_connection_action; +std::ostream& operator<<(std::ostream& os, next_connection_action v); } // namespace detail } // namespace mysql diff --git a/test/unit/src/printing.cpp b/test/unit/src/printing.cpp new file mode 100644 index 00000000..440421ba --- /dev/null +++ b/test/unit/src/printing.cpp @@ -0,0 +1,206 @@ +// +// Copyright (c) 2019-2024 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 +#include + +#include + +#include "test_common/printing.hpp" +#include "test_unit/printing.hpp" + +using namespace boost::mysql; + +// address_type +static const char* to_string(address_type v) +{ + switch (v) + { + case address_type::host_and_port: return "address_type::host_and_port"; + case address_type::unix_path: return "address_type::unix_path"; + default: return ""; + } +} + +std::ostream& boost::mysql::operator<<(std::ostream& os, address_type v) { return os << ::to_string(v); } + +// capabilities +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, const capabilities& v) +{ + return os << "capabilities{" << v.get() << "}"; +} + +// db_flavor +static const char* to_string(detail::db_flavor v) +{ + switch (v) + { + case detail::db_flavor::mysql: return "db_flavor::mysql"; + case detail::db_flavor::mariadb: return "db_flavor::mariadb"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, db_flavor v) { return os << ::to_string(v); } + +// resultset_encoding +static const char* to_string(detail::resultset_encoding v) +{ + switch (v) + { + case detail::resultset_encoding::text: return "resultset_encoding::text"; + case detail::resultset_encoding::binary: return "resultset_encoding::binary"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, detail::resultset_encoding v) +{ + return os << ::to_string(v); +} + +// results_iterator +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, const results_iterator& it) +{ + return os << "results_iterator{ .self = " << static_cast(it.obj()) + << ", .index = " << it.index() << "}"; +} + +// next_action_type; +static const char* to_string(detail::next_action_type v) +{ + switch (v) + { + case detail::next_action_type::none: return "next_action_type::none"; + case detail::next_action_type::read: return "next_action_type::read"; + case detail::next_action_type::write: return "next_action_type::write"; + case detail::next_action_type::ssl_handshake: return "next_action_type::ssl_handshake"; + case detail::next_action_type::ssl_shutdown: return "next_action_type::ssh_shutdown"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, next_action_type v) +{ + return os << ::to_string(v); +} + +// pipeline_stage_kind +static const char* to_string(detail::pipeline_stage_kind v) +{ + switch (v) + { + case detail::pipeline_stage_kind::execute: return "pipeline_stage_kind::execute"; + case detail::pipeline_stage_kind::prepare_statement: return "pipeline_stage_kind::prepare_statement"; + case detail::pipeline_stage_kind::close_statement: return "pipeline_stage_kind::close_statement"; + case detail::pipeline_stage_kind::reset_connection: return "pipeline_stage_kind::reset_connection"; + case detail::pipeline_stage_kind::set_character_set: return "pipeline_stage_kind::set_character_set"; + case detail::pipeline_stage_kind::ping: return "pipeline_stage_kind::ping"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, pipeline_stage_kind v) +{ + return os << ::to_string(v); +} + +// pipeline_request_stage +bool boost::mysql::detail::operator==(const pipeline_request_stage& lhs, const pipeline_request_stage& rhs) +{ + if (lhs.kind != rhs.kind || lhs.seqnum != rhs.seqnum) + return false; + switch (lhs.kind) + { + case pipeline_stage_kind::execute: return lhs.stage_specific.enc == rhs.stage_specific.enc; + case pipeline_stage_kind::set_character_set: + return lhs.stage_specific.charset == rhs.stage_specific.charset; + default: return true; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, pipeline_request_stage v) +{ + os << "pipeline_request_stage{ .kind = " << v.kind << ", .seqnum = " << +v.seqnum; + switch (v.kind) + { + case pipeline_stage_kind::execute: os << ", .enc = " << v.stage_specific.enc; break; + case pipeline_stage_kind::set_character_set: os << ", .charset = " << v.stage_specific.charset; break; + default: break; + } + return os << " }"; +} + +// connection_status +static const char* to_string(detail::connection_status v) +{ + switch (v) + { + case detail::connection_status::initial: return "connection_status::initial"; + case detail::connection_status::connect_in_progress: return "connection_status::connect_in_progress"; + case detail::connection_status::sleep_connect_failed_in_progress: + return "connection_status::sleep_connect_failed_in_progress"; + case detail::connection_status::reset_in_progress: return "connection_status::reset_in_progress"; + case detail::connection_status::ping_in_progress: return "connection_status::ping_in_progress"; + case detail::connection_status::idle: return "connection_status::idle"; + case detail::connection_status::in_use: return "connection_status::in_use"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, connection_status v) +{ + return os << ::to_string(v); +} + +// collection_state +static const char* to_string(detail::collection_state v) +{ + switch (v) + { + case detail::collection_state::needs_collect: return "collection_state::needs_collect"; + case detail::collection_state::needs_collect_with_reset: + return "collection_state::needs_collect_with_reset"; + case detail::collection_state::none: return "collection_state::none"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, collection_state v) +{ + return os << ::to_string(v); +} + +static const char* to_string(detail::next_connection_action v) +{ + switch (v) + { + case detail::next_connection_action::none: return "next_connection_action::none"; + case detail::next_connection_action::connect: return "next_connection_action::connect"; + case detail::next_connection_action::sleep_connect_failed: + return "next_connection_action::sleep_connect_failed"; + case detail::next_connection_action::idle_wait: return "next_connection_action::idle_wait"; + case detail::next_connection_action::reset: return "next_connection_action::reset"; + case detail::next_connection_action::ping: return "next_connection_action::ping"; + default: return ""; + } +} + +std::ostream& boost::mysql::detail::operator<<(std::ostream& os, next_connection_action v) +{ + return os << ::to_string(v); +} diff --git a/test/unit/test/connection_pool.cpp b/test/unit/test/connection_pool.cpp index adee2b99..29defc1c 100644 --- a/test/unit/test/connection_pool.cpp +++ b/test/unit/test/connection_pool.cpp @@ -23,7 +23,7 @@ #include #include "test_common/printing.hpp" -#include "test_unit/pool_printing.hpp" +#include "test_unit/printing.hpp" using namespace boost::mysql; namespace asio = boost::asio; @@ -47,7 +47,8 @@ struct pooled_connection_fixture pool->params(), ctx.get_executor(), ctx.get_executor(), - pool->shared_state() + pool->shared_state(), + &pool->reset_pipeline_request() )}; } diff --git a/test/unit/test/connection_pool/connection_pool_impl.cpp b/test/unit/test/connection_pool/connection_pool_impl.cpp index 29daef76..44a15187 100644 --- a/test/unit/test/connection_pool/connection_pool_impl.cpp +++ b/test/unit/test/connection_pool/connection_pool_impl.cpp @@ -16,6 +16,9 @@ #include #include +#include +#include + #include #include #include @@ -32,18 +35,20 @@ #include #include #include +#include #include #include #include #include +#include #include #include "mock_timer.hpp" #include "test_common/create_diagnostics.hpp" #include "test_common/printing.hpp" #include "test_common/tracker_executor.hpp" -#include "test_unit/pool_printing.hpp" +#include "test_unit/printing.hpp" // These tests rely on channels, which are not compatible with this // See https://github.com/chriskohlhoff/asio/issues/1398 @@ -65,13 +70,24 @@ using std::chrono::steady_clock; BOOST_AUTO_TEST_SUITE(test_connection_pool_impl) -enum fn_type +enum class fn_type { connect, - reset, + pipeline, ping, }; +std::ostream& operator<<(std::ostream& os, fn_type t) +{ + switch (t) + { + case fn_type::connect: return os << "fn_type::connect"; + case fn_type::pipeline: return os << "fn_type::pipeline"; + case fn_type::ping: return os << "fn_type::ping"; + default: return os << ""; + } +} + // A mock for mysql::any_connection. This allows us to control // when and how operations like async_connect or async_ping complete, // make assertions on the passed parameters, and force error conditions. @@ -140,14 +156,6 @@ class mock_connection ); } - // We use an internal async_reset_connection that allows setting the character set - template - auto async_reset_with_charset(const character_set& charset, CompletionToken&& token) - -> decltype(op_impl(fn_type::reset, nullptr, std::forward(token))) - { - BOOST_TEST(charset.name == "utf8mb4"); // We must always use utf8mb4 - return op_impl(fn_type::reset, nullptr, std::forward(token)); - } } impl_; struct step_op @@ -205,12 +213,27 @@ public: } template - auto async_ping(CompletionToken&& token) - -> decltype(impl_.op_impl(fn_type::ping, nullptr, std::forward(token))) + auto async_ping(CompletionToken&& token + ) -> decltype(impl_.op_impl(fn_type::ping, nullptr, std::forward(token))) { return impl_.op_impl(fn_type::ping, nullptr, std::forward(token)); } + template + auto async_run_pipeline( + const PipelineRequest& req, + typename PipelineRequest::response_type&, + CompletionToken&& token + ) -> decltype(impl_.op_impl(fn_type::pipeline, nullptr, std::forward(token))) + { + auto req_view = detail::access::get_impl(req).to_view(); + BOOST_TEST(req_view.stages.size() == 2u); + BOOST_TEST(req_view.stages[0].kind == detail::pipeline_stage_kind::reset_connection); + BOOST_TEST(req_view.stages[1].kind == detail::pipeline_stage_kind::set_character_set); + BOOST_TEST(req_view.stages[1].stage_specific.charset.name == "utf8mb4"); + return impl_.op_impl(fn_type::pipeline, nullptr, std::forward(token)); + } + void step( fn_type expected_op_type, asio::any_completion_handler handler, @@ -629,7 +652,7 @@ BOOST_AUTO_TEST_CASE(lifecycle_reset_success) check_shared_st(error_code(), diagnostics(), 1, 0); // Successful reset makes the connection idle again - BOOST_ASIO_CORO_YIELD step(node, fn_type::reset); + BOOST_ASIO_CORO_YIELD step(node, fn_type::pipeline); wait_for_status(node, connection_status::idle); check_shared_st(error_code(), diagnostics(), 0, 1); } @@ -660,7 +683,7 @@ BOOST_AUTO_TEST_CASE(lifecycle_reset_error) // Reset fails. This triggers a reconnection. Diagnostics are not saved BOOST_ASIO_CORO_YIELD - step(node, fn_type::reset, common_server_errc::er_aborting_connection); + step(node, fn_type::pipeline, common_server_errc::er_aborting_connection); wait_for_status(node, connection_status::connect_in_progress); check_shared_st(error_code(), diagnostics(), 1, 0); @@ -739,7 +762,7 @@ BOOST_AUTO_TEST_CASE(lifecycle_reset_timeout_disabled) check_shared_st(error_code(), diagnostics(), 1, 0); // Reset succeeds - BOOST_ASIO_CORO_YIELD step(node, fn_type::reset); + BOOST_ASIO_CORO_YIELD step(node, fn_type::pipeline); wait_for_status(node, connection_status::idle); check_shared_st(error_code(), diagnostics(), 0, 1); } @@ -1207,7 +1230,7 @@ BOOST_AUTO_TEST_CASE(get_connection_multiple_requests) // A connection is returned. The first task to enter is served return_connection(*node1, true); - BOOST_ASIO_CORO_YIELD step(*node1, fn_type::reset); + BOOST_ASIO_CORO_YIELD step(*node1, fn_type::pipeline); BOOST_ASIO_CORO_YIELD wait_for_task(task3, *node1); // The next connection to be returned is for task5 diff --git a/test/unit/test/connection_pool/sansio_connection_node.cpp b/test/unit/test/connection_pool/sansio_connection_node.cpp index 639b015b..c6aa9384 100644 --- a/test/unit/test/connection_pool/sansio_connection_node.cpp +++ b/test/unit/test/connection_pool/sansio_connection_node.cpp @@ -14,7 +14,7 @@ #include -#include "test_unit/pool_printing.hpp" +#include "test_unit/printing.hpp" using namespace boost::mysql::detail; using boost::mysql::client_errc; diff --git a/test/unit/test/detail/engine_impl.cpp b/test/unit/test/detail/engine_impl.cpp index f83b236f..76e7291b 100644 --- a/test/unit/test/detail/engine_impl.cpp +++ b/test/unit/test/detail/engine_impl.cpp @@ -236,7 +236,7 @@ BOOST_AUTO_TEST_CASE(next_action_read) tc.fn(eng, any_resumable_ref(algo)).validate_no_error(); BOOST_TEST(eng.stream().calls.size() == 1u); - BOOST_TEST(eng.stream().calls[0].type() == next_action::type_t::read); + BOOST_TEST(eng.stream().calls[0].type() == next_action_type::read); BOOST_TEST(eng.stream().calls[0].read_args().use_ssl == tc.ssl_active); BOOST_TEST(eng.stream().calls[0].read_args().buffer.data() == buff.data()); BOOST_TEST(eng.stream().calls[0].read_args().buffer.size() == buff.size()); @@ -276,7 +276,7 @@ BOOST_AUTO_TEST_CASE(next_action_write) tc.fn(eng, any_resumable_ref(algo)).validate_no_error(); BOOST_TEST(eng.stream().calls.size() == 1u); - BOOST_TEST(eng.stream().calls[0].type() == next_action::type_t::write); + BOOST_TEST(eng.stream().calls[0].type() == next_action_type::write); BOOST_TEST(eng.stream().calls[0].write_args().use_ssl == tc.ssl_active); BOOST_TEST(eng.stream().calls[0].write_args().buffer.data() == buff.data()); BOOST_TEST(eng.stream().calls[0].write_args().buffer.size() == buff.size()); @@ -436,7 +436,7 @@ BOOST_AUTO_TEST_CASE(resume_error_successive_calls) tc.fn(eng, any_resumable_ref(algo)).validate_error_exact(tc.ec); BOOST_TEST(eng.stream().calls.size() == 1u); - BOOST_TEST(eng.stream().calls[0].type() == next_action::type_t::connect); + BOOST_TEST(eng.stream().calls[0].type() == next_action_type::connect); algo.check_calls({ {error_code(), 0u}, {error_code(), 0u} diff --git a/test/unit/test/detail/typing/readable_field_traits.cpp b/test/unit/test/detail/typing/readable_field_traits.cpp index aa613ff7..069e542d 100644 --- a/test/unit/test/detail/typing/readable_field_traits.cpp +++ b/test/unit/test/detail/typing/readable_field_traits.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include "test_common/create_basic.hpp" #include "test_common/printing.hpp" @@ -53,6 +52,9 @@ using boost::mysql::detail::name_table_t; using boost::mysql::detail::pos_absent; using boost::mysql::detail::readable_field_traits; +// Don't attempt to print std::chrono values +BOOST_TEST_DONT_PRINT_LOG_VALUE(boost::mysql::time) + BOOST_AUTO_TEST_SUITE(test_readable_field_traits) // diff --git a/test/unit/test/field.cpp b/test/unit/test/field.cpp index 5686c939..6bc4d2c9 100644 --- a/test/unit/test/field.cpp +++ b/test/unit/test/field.cpp @@ -11,18 +11,18 @@ #include -#include #include -#include #include "test_common/assert_buffer_equals.hpp" #include "test_common/create_basic.hpp" -#include "test_common/printing.hpp" #include "test_common/stringize.hpp" using namespace boost::mysql; using namespace boost::mysql::test; +// Don't attempt to print std::chrono values +BOOST_TEST_DONT_PRINT_LOG_VALUE(boost::mysql::time) + BOOST_AUTO_TEST_SUITE(test_field) BOOST_AUTO_TEST_SUITE(constructors) diff --git a/test/unit/test/field_view.cpp b/test/unit/test/field_view.cpp index f8a7e98c..851244c1 100644 --- a/test/unit/test/field_view.cpp +++ b/test/unit/test/field_view.cpp @@ -21,11 +21,13 @@ #include "test_common/assert_buffer_equals.hpp" #include "test_common/create_basic.hpp" -#include "test_common/printing.hpp" using namespace boost::mysql; using namespace boost::mysql::test; +// Don't attempt to print std::chrono values +BOOST_TEST_DONT_PRINT_LOG_VALUE(boost::mysql::time) + BOOST_AUTO_TEST_SUITE(test_field_view) BOOST_AUTO_TEST_SUITE(constructors) @@ -645,7 +647,7 @@ BOOST_AUTO_TEST_CASE(operator_stream) field_view input; string_view expected; } test_cases[] = { - // Field views holding values + // Field views holding values {"null", field_view(), ""}, {"i64_positive", field_view(std::int64_t(42)), "42"}, {"i64_negative", field_view(std::int64_t(-90)), "-90"}, @@ -666,7 +668,7 @@ BOOST_AUTO_TEST_CASE(operator_stream) {"datetime", field_view(datetime(2020, 1, 19, 11, 30, 21, 98765)), "2020-01-19 11:30:21.098765"}, {"time", field_view(-maket(12, 1, 5, 345)), "-12:01:05.000345"}, - // Field views holding pointers to fields + // Field views holding pointers to fields {"ref_null", field_view(owning_fields.f_null), ""}, {"ref_int64", field_view(owning_fields.f_int64), "-1"}, {"ref_uint64", field_view(owning_fields.f_uint64), "50"}, diff --git a/test/unit/test/metadata.cpp b/test/unit/test/metadata.cpp index cd5b710d..09f06971 100644 --- a/test/unit/test/metadata.cpp +++ b/test/unit/test/metadata.cpp @@ -12,7 +12,8 @@ #include #include -#include "test_common/printing.hpp" +#include + #include "test_unit/create_meta.hpp" using namespace boost::mysql; diff --git a/test/unit/test/pipeline.cpp b/test/unit/test/pipeline.cpp new file mode 100644 index 00000000..ee9e5fa9 --- /dev/null +++ b/test/unit/test/pipeline.cpp @@ -0,0 +1,767 @@ +// +// Copyright (c) 2019-2024 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 + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_common/assert_buffer_equals.hpp" +#include "test_common/buffer_concat.hpp" +#include "test_common/create_basic.hpp" +#include "test_common/create_diagnostics.hpp" +#include "test_common/printing.hpp" +#include "test_unit/create_execution_processor.hpp" +#include "test_unit/create_frame.hpp" +#include "test_unit/create_ok.hpp" +#include "test_unit/create_query_frame.hpp" +#include "test_unit/create_statement.hpp" +#include "test_unit/printing.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using boost::test_tools::per_element; +using detail::pipeline_request_stage; +using detail::pipeline_stage_kind; +using detail::resultset_encoding; + +BOOST_AUTO_TEST_SUITE(test_pipeline) + +// Validates the exception thrown when adding a statement execution with wrong number of params +static auto stmt_exc_validator = [](const std::invalid_argument& exc) { + BOOST_TEST( + string_view(exc.what()) == "Wrong number of actual parameters supplied to a prepared statement" + ); + return true; +}; + +BOOST_AUTO_TEST_SUITE(any_stage_response_) + +BOOST_AUTO_TEST_CASE(default_ctor) +{ + // Construct + any_stage_response r; + + // Contains an empty error + BOOST_TEST(!r.has_results()); + BOOST_TEST(!r.has_statement()); + BOOST_TEST(r.error() == errcode_with_diagnostics{}); +} + +BOOST_AUTO_TEST_CASE(underlying_error) +{ + // Setup + any_stage_response r; + detail::access::get_impl(r).set_error(client_errc::invalid_encoding, create_server_diag("my_message")); + + // Check + BOOST_TEST(!r.has_results()); + BOOST_TEST(!r.has_statement()); + BOOST_TEST( + r.error() == + (errcode_with_diagnostics{client_errc::invalid_encoding, create_server_diag("my_message")}) + ); +} + +BOOST_AUTO_TEST_CASE(underlying_statement) +{ + // Setup + any_stage_response r; + detail::access::get_impl(r).set_result(statement_builder().id(3).build()); + + // Check + BOOST_TEST(!r.has_results()); + BOOST_TEST(r.has_statement()); + BOOST_TEST(r.as_statement().id() == 3u); + BOOST_TEST(r.get_statement().id() == 3u); + + // error() can be called and returns an empty error + BOOST_TEST(r.error() == errcode_with_diagnostics{}); +} + +BOOST_AUTO_TEST_CASE(underlying_results) +{ + // Setup + any_stage_response r; + detail::access::get_impl(r).emplace_results(); + add_ok(detail::access::get_impl(r).get_processor(), ok_builder().info("some_info").build()); + + // Check + BOOST_TEST(r.has_results()); + BOOST_TEST(!r.has_statement()); + BOOST_TEST(r.get_results().info() == "some_info"); + BOOST_TEST(r.as_results().info() == "some_info"); + + // Rvalue reference accessors work + results&& ref1 = std::move(r).get_results(); + results&& ref2 = std::move(r).as_results(); + boost::ignore_unused(ref1); + boost::ignore_unused(ref2); + + // error() can be called and returns an empty error + BOOST_TEST(r.error() == errcode_with_diagnostics{}); +} + +BOOST_AUTO_TEST_CASE(as_results_error) +{ + // Empty error + any_stage_response r; + BOOST_CHECK_THROW(r.as_results(), std::invalid_argument); + BOOST_CHECK_THROW(std::move(r).as_results(), std::invalid_argument); + + // Non-empty error + detail::access::get_impl(r).set_error(client_errc::extra_bytes, create_client_diag("my_msg")); + BOOST_CHECK_THROW(r.as_results(), std::invalid_argument); + BOOST_CHECK_THROW(std::move(r).as_results(), std::invalid_argument); + + // Statement + detail::access::get_impl(r).set_result(statement_builder().build()); + BOOST_CHECK_THROW(r.as_results(), std::invalid_argument); + BOOST_CHECK_THROW(std::move(r).as_results(), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(as_statement_error) +{ + // Empty error + any_stage_response r; + BOOST_CHECK_THROW(r.as_statement(), std::invalid_argument); + + // Non-empty error + detail::access::get_impl(r).set_error(client_errc::extra_bytes, create_client_diag("my_msg")); + BOOST_CHECK_THROW(r.as_statement(), std::invalid_argument); + + // results + detail::access::get_impl(r).emplace_results(); + BOOST_CHECK_THROW(r.as_statement(), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(change_type) +{ + any_stage_response r; + + // Set results + detail::access::get_impl(r).emplace_results(); + BOOST_TEST(r.has_results()); + + // Set an error + detail::access::get_impl(r).set_error(client_errc::extra_bytes, create_client_diag("abc")); + BOOST_TEST(!r.has_results()); + BOOST_TEST(r.error() == (errcode_with_diagnostics{client_errc::extra_bytes, create_client_diag("abc")})); + + // Reset the error + detail::access::get_impl(r).emplace_error(); + BOOST_TEST(r.error() == errcode_with_diagnostics{}); + + // Set a statement + detail::access::get_impl(r).set_result(statement_builder().build()); + BOOST_TEST(r.has_statement()); + + // Set results again + detail::access::get_impl(r).emplace_results(); + BOOST_TEST(r.has_results()); + BOOST_TEST(!r.has_statement()); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(stage_creation) + +// Helper to run any successful stage creation test +template +static void check_stage_creation( + PipelineStageType stage, + const std::vector& expected_buffer, + pipeline_request_stage expected_stage +) +{ + // Serialize the request + std::vector buff{0xde, 0xad}; + auto actual_stage = detail::pipeline_stage_access::create(stage, buff); + + // Check + std::vector expected{0xde, 0xad}; + expected.insert(expected.end(), expected_buffer.begin(), expected_buffer.end()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected); + BOOST_TEST(actual_stage == expected_stage); +} + +BOOST_AUTO_TEST_CASE(execute_text_query) +{ + check_stage_creation( + execute_stage("SELECT 1"), + create_query_frame(0, "SELECT 1"), + {pipeline_stage_kind::execute, 1u, resultset_encoding::text} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_individual_parameters) +{ + check_stage_creation( + execute_stage(statement_builder().id(2).num_params(3).build(), {42, "abc", nullptr}), + {0x1e, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x04, 0x01, 0x08, 0x00, 0xfe, 0x00, 0x06, 0x00, 0x2a, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x61, 0x62, 0x63}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_individual_fields_no_params) +{ + // We run the required writable field transformations + check_stage_creation( + execute_stage(statement_builder().id(2).num_params(0).build(), {}), + {0x0a, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_individual_fields_writable_fields) +{ + // We run the required writable field transformations + check_stage_creation( + execute_stage( + statement_builder().id(2).num_params(3).build(), + {boost::optional(42), "abc", boost::optional()} + ), + {0x1e, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x04, 0x01, 0x08, 0x00, 0xfe, 0x00, 0x06, 0x00, 0x2a, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x61, 0x62, 0x63}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_individual_fields_manyfields) +{ + // There is an optimization to avoid allocations for statements with less than 64 fields + check_stage_creation( + execute_stage( + statement_builder().id(1).num_params(65).build(), + {"1", "2", "3", nullptr, nullptr, nullptr, "a", "b", "c", nullptr, + nullptr, nullptr, 13, 14, nullptr, nullptr, nullptr, 18, "h", "jk", + 21, nullptr, nullptr, nullptr, 25, nullptr, nullptr, nullptr, 29, 30, + nullptr, nullptr, nullptr, 34, 35, nullptr, nullptr, nullptr, 39, 40, + nullptr, nullptr, nullptr, 44, 45, nullptr, nullptr, nullptr, 49, 50, + nullptr, nullptr, nullptr, 54, nullptr, nullptr, nullptr, 58, nullptr, nullptr, + nullptr, 62, "U", "VV", nullptr} + ), + {0x3c, 0x01, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0xce, 0xe1, + 0xce, 0x39, 0xe7, 0xdc, 0x1d, 0x01, 0x01, 0xfe, 0x00, 0xfe, 0x00, 0xfe, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x06, 0x00, 0xfe, 0x00, 0xfe, 0x00, 0xfe, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, + 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0xfe, 0x00, 0xfe, 0x00, 0x08, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x06, + 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, + 0x00, 0x08, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x08, 0x00, 0xfe, 0x00, 0xfe, 0x00, 0x06, + 0x00, 0x01, 0x31, 0x01, 0x32, 0x01, 0x33, 0x01, 0x61, 0x01, 0x62, 0x01, 0x63, 0x0d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x68, 0x02, 0x6a, 0x6b, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x55, 0x02, 0x56, 0x56}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_range) +{ + auto fv_arr = make_fv_arr(42, "abc", nullptr); + check_stage_creation( + execute_stage(statement_builder().id(2).num_params(3).build(), fv_arr), + {0x1e, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x04, 0x01, 0x08, 0x00, 0xfe, 0x00, 0x06, 0x00, 0x2a, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x61, 0x62, 0x63}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary} + ); +} + +BOOST_AUTO_TEST_CASE(execute_statement_error) +{ + std::vector buff; + + // Individual fields, too few parameters + BOOST_CHECK_EXCEPTION( + detail::pipeline_stage_access::create( + execute_stage(statement_builder().num_params(2).build(), {10}), + buff + ), + std::invalid_argument, + stmt_exc_validator + ); + + // Individual fields, too many parameters + BOOST_CHECK_EXCEPTION( + detail::pipeline_stage_access::create( + execute_stage(statement_builder().num_params(2).build(), {10, 20, 30}), + buff + ), + std::invalid_argument, + stmt_exc_validator + ); + + // Range, too few parameters + auto fv_arr = make_fv_arr(42); + BOOST_CHECK_EXCEPTION( + detail::pipeline_stage_access::create( + execute_stage( + statement_builder().num_params(2).build(), + {fv_arr.data(), fv_arr.data() + fv_arr.size()} + ), + buff + ), + std::invalid_argument, + stmt_exc_validator + ); +} + +// +// prepare statement +// +BOOST_AUTO_TEST_CASE(prepare_statement) +{ + check_stage_creation( + prepare_statement_stage("SELECT 1"), + create_prepare_statement_frame(0, "SELECT 1"), + {pipeline_stage_kind::prepare_statement, 1u, {}} + ); +} + +BOOST_AUTO_TEST_CASE(prepare_statement_empty) +{ + // Empty prepare statements don't produce problems + check_stage_creation( + prepare_statement_stage(""), + create_prepare_statement_frame(0, ""), + {pipeline_stage_kind::prepare_statement, 1u, {}} + ); +} + +// +// close statement +// +BOOST_AUTO_TEST_CASE(close_statement) +{ + check_stage_creation( + close_statement_stage(statement_builder().id(3).num_params(1).build()), + create_frame(0, {0x19, 0x03, 0x00, 0x00, 0x00}), + {pipeline_stage_kind::close_statement, 1u, {}} + ); +} + +// +// reset connection +// +BOOST_AUTO_TEST_CASE(reset_connection) +{ + check_stage_creation( + reset_connection_stage(), + create_frame(0, {0x1f}), + {pipeline_stage_kind::reset_connection, 1u, {}} + ); +} + +// +// set character set +// +BOOST_AUTO_TEST_CASE(set_character_set) +{ + check_stage_creation( + set_character_set_stage(utf8mb4_charset), + create_query_frame(0, "SET NAMES 'utf8mb4'"), + {pipeline_stage_kind::set_character_set, 1u, utf8mb4_charset} + ); +} + +BOOST_AUTO_TEST_CASE(set_character_set_escapes) +{ + // We don't create SQL injection vulnerabilities while composing SET NAMES + character_set charset{"inj'ection", utf8mb4_charset.next_char}; + check_stage_creation( + set_character_set_stage(charset), + create_query_frame(0, "SET NAMES 'inj\\'ection'"), + {pipeline_stage_kind::set_character_set, 1u, charset} + ); +} + +BOOST_AUTO_TEST_CASE(set_character_set_error) +{ + // If a character set name that can't be securely escaped gets passed, we throw + std::vector buff; + + BOOST_CHECK_EXCEPTION( + detail::pipeline_stage_access::create( + set_character_set_stage(character_set{"bad\xff", utf8mb4_charset.next_char}), + buff + ), + std::invalid_argument, + [](const std::invalid_argument& exc) { + BOOST_TEST(string_view(exc.what()) == "Invalid character set name"); + return true; + } + ); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(dynamic_interface) + +// Adding stages work +BOOST_AUTO_TEST_CASE(add) +{ + // Default ctor creates an empty request + pipeline_request req; + auto view = detail::access::get_impl(req).to_view(); + std::vector expected_stages; + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, blob{}); + + // Add a reset connection stage + req.add(reset_connection_stage()); + view = detail::access::get_impl(req).to_view(); + expected_stages = { + {pipeline_stage_kind::reset_connection, 1u, {}} + }; + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, create_frame(0, {0x1f})); + + // Add an execution stage + req.add(execute_stage("SELECT 1")); + view = detail::access::get_impl(req).to_view(); + expected_stages = { + {pipeline_stage_kind::reset_connection, 1u, {} }, + {pipeline_stage_kind::execute, 1u, resultset_encoding::text} + }; + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS( + view.buffer, + concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SELECT 1")) + ); +} + +// All stage types work +BOOST_AUTO_TEST_CASE(all_stage_kinds) +{ + // Setup + pipeline_request req; + + // Add stages + req.add(reset_connection_stage()) + .add(execute_stage("SELECT 1")) + .add(prepare_statement_stage("SELECT ?")) + .add(set_character_set_stage(utf8mb4_charset)) + .add(close_statement_stage(statement_builder().id(8).build())); + + // Expected values + auto expected_buffer = buffer_builder() + .add(create_frame(0, {0x1f})) + .add(create_query_frame(0, "SELECT 1")) + .add(create_prepare_statement_frame(0, "SELECT ?")) + .add(create_query_frame(0, "SET NAMES 'utf8mb4'")) + .add(create_frame(0, {0x19, 0x08, 0x00, 0x00, 0x00})) + .build(); + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::reset_connection, 1u, {} }, + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + {pipeline_stage_kind::prepare_statement, 1u, {} }, + {pipeline_stage_kind::set_character_set, 1u, utf8mb4_charset }, + {pipeline_stage_kind::close_statement, 1u, {} }, + }; + + // Check + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, expected_buffer); +} + +BOOST_AUTO_TEST_CASE(clear_previous_contents) +{ + // Create a pipeline request with some steps + pipeline_request req; + req.add(reset_connection_stage()) + .add(set_character_set_stage(utf8mb4_charset)) + .add(execute_stage("SELECT 1")); + + // Clear the pipeline + req.clear(); + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == std::vector{}, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, blob{}); + + // Add some stages again + req.add(execute_stage("abc")).add(close_statement_stage(statement_builder().id(7).build())); + + // Check + view = detail::access::get_impl(req).to_view(); + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + {pipeline_stage_kind::close_statement, 1u, {} }, + }; + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS( + view.buffer, + concat_copy(create_query_frame(0, "abc"), create_frame(0, {0x19, 0x07, 0x00, 0x00, 0x00})) + ); +} + +BOOST_AUTO_TEST_CASE(clear_empty) +{ + // Clearing an empty pipeline is a no-op + pipeline_request req; + req.clear(); + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == std::vector{}, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, blob{}); +} + +BOOST_AUTO_TEST_CASE(add_error) +{ + // Spotcheck: add propagates errors raised by serialization functions + pipeline_request req; + BOOST_CHECK_EXCEPTION( + req.add(execute_stage(statement_builder().num_params(2).build(), {42})), + std::invalid_argument, + stmt_exc_validator + ); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(static_interface) + +BOOST_AUTO_TEST_CASE(all_stage_kinds) +{ + // Setup + auto req = make_pipeline_request( + reset_connection_stage(), + execute_stage("SELECT 1"), + prepare_statement_stage("SELECT ?"), + set_character_set_stage(utf8mb4_charset), + close_statement_stage(statement_builder().id(8).build()) + ); + using expected_type = static_pipeline_request< + reset_connection_stage, + execute_stage, + prepare_statement_stage, + set_character_set_stage, + close_statement_stage>; + static_assert(std::is_same::value, "Type deduction error"); + + // Expected values + auto expected_buffer = buffer_builder() + .add(create_frame(0, {0x1f})) + .add(create_query_frame(0, "SELECT 1")) + .add(create_prepare_statement_frame(0, "SELECT ?")) + .add(create_query_frame(0, "SET NAMES 'utf8mb4'")) + .add(create_frame(0, {0x19, 0x08, 0x00, 0x00, 0x00})) + .build(); + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::reset_connection, 1u, {} }, + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + {pipeline_stage_kind::prepare_statement, 1u, {} }, + {pipeline_stage_kind::set_character_set, 1u, utf8mb4_charset }, + {pipeline_stage_kind::close_statement, 1u, {} }, + }; + + // Check + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, expected_buffer); +} + +BOOST_AUTO_TEST_CASE(repeated_stage_kinds) +{ + // Spotcheck: repeated stage kinds don't create problems + auto req = make_pipeline_request( + execute_stage("SELECT 1"), + execute_stage("SELECT 2"), + prepare_statement_stage("SELECT ?"), + prepare_statement_stage("COMMIT") + ); + using expected_type = static_pipeline_request< + execute_stage, + execute_stage, + prepare_statement_stage, + prepare_statement_stage>; + static_assert(std::is_same::value, "Type deduction error"); + + // Expected values + auto expected_buffer = buffer_builder() + .add(create_query_frame(0, "SELECT 1")) + .add(create_query_frame(0, "SELECT 2")) + .add(create_prepare_statement_frame(0, "SELECT ?")) + .add(create_prepare_statement_frame(0, "COMMIT")) + .build(); + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + {pipeline_stage_kind::prepare_statement, 1u, {} }, + {pipeline_stage_kind::prepare_statement, 1u, {} }, + }; + + // Check + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, expected_buffer); +} + +BOOST_AUTO_TEST_CASE(statement_execution_lifetimes) +{ + // Spotcheck: we don't create lifetime problems when adding statement executions + std::string param = "abc"; + auto req = make_pipeline_request( + execute_stage(statement_builder().id(2).num_params(3).build(), {42, param, nullptr}) + ); + param = "uuu"; + + // Expected values + const std::uint8_t expected_buffer[] = {0x1e, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x08, 0x00, + 0xfe, 0x00, 0x06, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x61, 0x62, 0x63}; + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::execute, 1u, resultset_encoding::binary}, + }; + + // Check + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, expected_buffer); +} + +BOOST_AUTO_TEST_CASE(assignment) +{ + // Spotcheck: assignment works as expected + // Setup + auto req = make_pipeline_request( + prepare_statement_stage("START TRANSACTION"), + execute_stage(statement_builder().id(1).num_params(0).build(), {}) + ); + req = {prepare_statement_stage("COMMIT"), execute_stage("SELECT 2")}; + + // Expected values + auto expected_buffer = buffer_builder() + .add(create_prepare_statement_frame(0, "COMMIT")) + .add(create_query_frame(0, "SELECT 2")) + .build(); + const pipeline_request_stage expected_stages[] = { + {pipeline_stage_kind::prepare_statement, 1u, {} }, + {pipeline_stage_kind::execute, 1u, resultset_encoding::text}, + }; + + // Check + auto view = detail::access::get_impl(req).to_view(); + BOOST_TEST(view.stages == expected_stages, per_element()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(view.buffer, expected_buffer); +} + +BOOST_AUTO_TEST_CASE(ctor_error) +{ + // Spotcheck: ctor propagates errors raised by serialization functions + BOOST_CHECK_EXCEPTION( + make_pipeline_request(execute_stage(statement_builder().num_params(2).build(), {42})), + std::invalid_argument, + stmt_exc_validator + ); +} + +#ifndef BOOST_NO_CXX17_DEDUCTION_GUIDES +BOOST_AUTO_TEST_CASE(deduction_guide) +{ + static_pipeline_request req( + reset_connection_stage(), + execute_stage("SELECT 1"), + prepare_statement_stage("SELECT ?"), + set_character_set_stage(utf8mb4_charset), + close_statement_stage(statement_builder().id(8).build()), + execute_stage("SELECT 1") + ); + using expected_type = static_pipeline_request< + reset_connection_stage, + execute_stage, + prepare_statement_stage, + set_character_set_stage, + close_statement_stage, + execute_stage>; + static_assert(std::is_same::value, "Deduction guide is incorrect"); + boost::ignore_unused(req); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() + +// errcode_with_diagnostics can be used with system::result +BOOST_AUTO_TEST_CASE(errcode_with_diagnostics_result) +{ + boost::system::result r = errcode_with_diagnostics{ + client_errc::cancelled, + create_server_diag("my_message") + }; + + BOOST_CHECK_EXCEPTION(r.value(), error_with_diagnostics, [](const error_with_diagnostics& exc) { + BOOST_TEST(exc.code() == client_errc::cancelled); + BOOST_TEST(exc.get_diagnostics() == create_server_diag("my_message")); + return true; + }); +} + +// pipeline concepts +using detail::is_pipeline_stage_type; +static_assert(is_pipeline_stage_type::value, ""); +static_assert(is_pipeline_stage_type::value, ""); +static_assert(is_pipeline_stage_type::value, ""); +static_assert(is_pipeline_stage_type::value, ""); +static_assert(is_pipeline_stage_type::value, ""); +static_assert(!is_pipeline_stage_type::value, ""); +static_assert(!is_pipeline_stage_type::value, ""); +static_assert(!is_pipeline_stage_type::value, ""); +static_assert(!is_pipeline_stage_type::value, ""); +static_assert(!is_pipeline_stage_type::value, ""); + +using detail::is_pipeline_request_type; +static_assert(is_pipeline_request_type::value, ""); +static_assert(is_pipeline_request_type>::value, ""); +static_assert(is_pipeline_request_type>::value, ""); +static_assert( + is_pipeline_request_type>::value, + "" +); +static_assert(!is_pipeline_request_type::value, ""); +static_assert(!is_pipeline_request_type::value, ""); +static_assert(!is_pipeline_request_type::value, ""); +static_assert(!is_pipeline_request_type::value, ""); + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/close_statement.cpp b/test/unit/test/sansio/close_statement.cpp index 958c314d..0d597912 100644 --- a/test/unit/test/sansio/close_statement.cpp +++ b/test/unit/test/sansio/close_statement.cpp @@ -11,6 +11,7 @@ #include #include +#include #include @@ -26,6 +27,8 @@ using namespace boost::mysql::test; using namespace boost::mysql; +// setup_close_statement_pipeline: running a pipeline with these parameters +// has the intended effect BOOST_AUTO_TEST_SUITE(test_close_statement) // A close_statement request pipelined with a ping (frame headers included) @@ -36,10 +39,7 @@ static std::vector expected_request() struct fixture : algo_fixture_base { - detail::close_statement_algo algo{ - st, - {&diag, 3} - }; + detail::run_pipeline_algo algo{detail::setup_close_statement_pipeline(st, {&diag, 3})}; }; BOOST_AUTO_TEST_CASE(success) diff --git a/test/unit/test/sansio/execute.cpp b/test/unit/test/sansio/execute.cpp index 7c73ebe6..c4ce8883 100644 --- a/test/unit/test/sansio/execute.cpp +++ b/test/unit/test/sansio/execute.cpp @@ -16,12 +16,10 @@ #include -#include "test_common/assert_buffer_equals.hpp" #include "test_common/buffer_concat.hpp" #include "test_common/check_meta.hpp" #include "test_unit/algo_test.hpp" #include "test_unit/create_coldef_frame.hpp" -#include "test_unit/create_execution_processor.hpp" #include "test_unit/create_frame.hpp" #include "test_unit/create_meta.hpp" #include "test_unit/create_ok.hpp" @@ -39,21 +37,156 @@ using boost::mysql::detail::resultset_encoding; BOOST_AUTO_TEST_SUITE(test_execute) -struct fixture : algo_fixture_base +// The serialized form of a SELECT 1 query request +static constexpr std::uint8_t serialized_select_1[] = {0x03, 0x53, 0x45, 0x4c, 0x45, 0x43, 0x54, 0x20, 0x31}; + +struct read_response_fixture : algo_fixture_base +{ + mock_execution_processor proc; + detail::read_execute_response_algo algo{&diag, &proc}; + + read_response_fixture() { proc.sequence_number() = 42; } +}; + +BOOST_AUTO_TEST_CASE(read_response_eof) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(create_ok_frame(42, ok_builder().affected_rows(60u).info("abc").build())) + .check(fix); + + // Verify + fix.proc.num_calls().on_head_ok_packet(1).validate(); + BOOST_TEST(fix.proc.affected_rows() == 60u); + BOOST_TEST(fix.proc.info() == "abc"); +} + +BOOST_AUTO_TEST_CASE(read_response_single_row_batch) +{ + // Setup + read_response_fixture fix; + + // Run the algo. Rows and OK are received in a single go (one call to read_some_rows) + algo_test() + .expect_read(create_frame(42, {0x01})) // OK, 1 column + .expect_read(create_coldef_frame(43, meta_builder().type(column_type::bigint).build_coldef())) + .expect_read(buffer_builder() + .add(create_text_row_message(44, 42)) + .add(create_text_row_message(45, 43)) + .add(create_eof_frame(46, ok_builder().affected_rows(10u).info("1st").build())) + .build()) + .check(fix); + + // Verify + fix.proc.num_calls() + .on_num_meta(1) + .on_meta(1) + .on_row_batch_start(1) + .on_row(2) + .on_row_batch_finish(1) + .on_row_ok_packet(1) + .validate(); + BOOST_TEST(fix.proc.encoding() == resultset_encoding::text); + BOOST_TEST(fix.proc.num_meta() == 1u); + check_meta(fix.proc.meta(), {column_type::bigint}); + BOOST_TEST(fix.proc.affected_rows() == 10u); + BOOST_TEST(fix.proc.info() == "1st"); +} + +BOOST_AUTO_TEST_CASE(read_response_multiple_row_batches) +{ + // Setup + read_response_fixture fix; + + // Run the algo. Multiple read_some_rows calls are required + algo_test() + .expect_read(create_frame(42, {0x01})) // OK, 1 column + .expect_read(create_coldef_frame(43, meta_builder().type(column_type::tinyint).build_coldef())) + .expect_read(create_text_row_message(44, 42)) + .expect_read(create_text_row_message(45, 43)) + .expect_read(create_eof_frame(46, ok_builder().affected_rows(10u).info("1st").build())) + .check(fix); + + // Verify + fix.proc.num_calls() + .on_num_meta(1) + .on_meta(1) + .on_row_batch_start(3) + .on_row(2) + .on_row_batch_finish(3) + .on_row_ok_packet(1) + .validate(); + BOOST_TEST(fix.proc.encoding() == resultset_encoding::text); + BOOST_TEST(fix.proc.num_meta() == 1u); + check_meta(fix.proc.meta(), {column_type::tinyint}); + BOOST_TEST(fix.proc.affected_rows() == 10u); + BOOST_TEST(fix.proc.info() == "1st"); +} + +BOOST_AUTO_TEST_CASE(read_response_multiple_resultsets) +{ + // Setup + read_response_fixture fix; + + // Run the algo. Multiple read_some_rows calls are required + algo_test() + .expect_read(create_frame(42, {0x01})) // OK, 1 column + .expect_read(create_coldef_frame(43, meta_builder().type(column_type::tinyint).build_coldef())) + .expect_read(create_text_row_message(44, 42)) + .expect_read( + create_eof_frame(45, ok_builder().affected_rows(10u).info("1st").more_results(true).build()) + ) + .expect_read(create_frame(46, {0x01})) // OK, 1 column + .expect_read(create_coldef_frame(47, meta_builder().type(column_type::varchar).build_coldef())) + .expect_read( + create_eof_frame(48, ok_builder().affected_rows(11u).info("2nd").more_results(true).build()) + ) + .expect_read(create_ok_frame(49, ok_builder().affected_rows(12u).info("3rd").build())) + .check(fix); + + // Verify + fix.proc.num_calls() + .on_num_meta(2) + .on_meta(2) + .on_row_batch_start(3) + .on_row(1) + .on_row_batch_finish(3) + .on_row_ok_packet(2) + .on_head_ok_packet(1) + .validate(); + BOOST_TEST(fix.proc.encoding() == resultset_encoding::text); + BOOST_TEST(fix.proc.num_meta() == 1u); + BOOST_TEST(fix.proc.affected_rows() == 12u); + BOOST_TEST(fix.proc.info() == "3rd"); +} + +// Tests error on write, while reading head and while reading rows (error spotcheck) +BOOST_AUTO_TEST_CASE(read_response_error_network_error) +{ + algo_test() + .expect_read(create_frame(42, {0x01})) // OK, 1 column + .expect_read(create_coldef_frame(43, meta_builder().type(column_type::tinyint).build_coldef())) + .expect_read(create_text_row_message(44, 42)) + .expect_read(create_text_row_message(45, 43)) + .expect_read(create_eof_frame(46, ok_builder().affected_rows(10u).info("1st").build())) + .check_network_errors(); +} + +struct execute_fixture : algo_fixture_base { mock_execution_processor proc; detail::execute_algo algo; - fixture(any_execution_request req = {"SELECT 1"}) : algo(st, {&diag, req, &proc}) {} + execute_fixture(any_execution_request req = {"SELECT 1"}) : algo({&diag, req, &proc}) {} }; -// The serialized form of a SELECT 1 query request -static constexpr std::uint8_t serialized_select_1[] = {0x03, 0x53, 0x45, 0x4c, 0x45, 0x43, 0x54, 0x20, 0x31}; - -BOOST_AUTO_TEST_CASE(eof) +BOOST_AUTO_TEST_CASE(execute_success_eof) { // Setup - fixture fix; + execute_fixture fix; // Run the algo algo_test() @@ -68,10 +201,10 @@ BOOST_AUTO_TEST_CASE(eof) BOOST_TEST(fix.proc.info() == "abc"); } -BOOST_AUTO_TEST_CASE(single_row_batch) +BOOST_AUTO_TEST_CASE(execute_success_rows) { // Setup - fixture fix; + execute_fixture fix; // Run the algo. Rows and OK are received in a single go (one call to read_some_rows) algo_test() @@ -102,84 +235,13 @@ BOOST_AUTO_TEST_CASE(single_row_batch) BOOST_TEST(fix.proc.info() == "1st"); } -BOOST_AUTO_TEST_CASE(multiple_row_batches) -{ - // Setup - fixture fix; - - // Run the algo. Multiple read_some_rows calls are required - algo_test() - .expect_write(create_frame(0, serialized_select_1)) - .expect_read(create_frame(1, {0x01})) // OK, 1 column - .expect_read(create_coldef_frame(2, meta_builder().type(column_type::tinyint).build_coldef())) - .expect_read(create_text_row_message(3, 42)) - .expect_read(create_text_row_message(4, 43)) - .expect_read(create_eof_frame(5, ok_builder().affected_rows(10u).info("1st").build())) - .check(fix); - - // Verify - fix.proc.num_calls() - .reset(1) - .on_num_meta(1) - .on_meta(1) - .on_row_batch_start(3) - .on_row(2) - .on_row_batch_finish(3) - .on_row_ok_packet(1) - .validate(); - BOOST_TEST(fix.proc.encoding() == resultset_encoding::text); - BOOST_TEST(fix.proc.num_meta() == 1u); - check_meta(fix.proc.meta(), {column_type::tinyint}); - BOOST_TEST(fix.proc.affected_rows() == 10u); - BOOST_TEST(fix.proc.info() == "1st"); -} - -BOOST_AUTO_TEST_CASE(multiple_resultsets) -{ - // Setup - fixture fix; - - // Run the algo. Multiple read_some_rows calls are required - algo_test() - .expect_write(create_frame(0, serialized_select_1)) - .expect_read(create_frame(1, {0x01})) // OK, 1 column - .expect_read(create_coldef_frame(2, meta_builder().type(column_type::tinyint).build_coldef())) - .expect_read(create_text_row_message(3, 42)) - .expect_read( - create_eof_frame(4, ok_builder().affected_rows(10u).info("1st").more_results(true).build()) - ) - .expect_read(create_frame(5, {0x01})) // OK, 1 column - .expect_read(create_coldef_frame(6, meta_builder().type(column_type::varchar).build_coldef())) - .expect_read( - create_eof_frame(7, ok_builder().affected_rows(11u).info("2nd").more_results(true).build()) - ) - .expect_read(create_ok_frame(8, ok_builder().affected_rows(12u).info("3rd").build())) - .check(fix); - - // Verify - fix.proc.num_calls() - .reset(1) - .on_num_meta(2) - .on_meta(2) - .on_row_batch_start(3) - .on_row(1) - .on_row_batch_finish(3) - .on_row_ok_packet(2) - .on_head_ok_packet(1) - .validate(); - BOOST_TEST(fix.proc.encoding() == resultset_encoding::text); - BOOST_TEST(fix.proc.num_meta() == 1u); - BOOST_TEST(fix.proc.affected_rows() == 12u); - BOOST_TEST(fix.proc.info() == "3rd"); -} - // Test that immediate completion with errors in start_execution are propagated correctlty -BOOST_AUTO_TEST_CASE(error_num_params) +BOOST_AUTO_TEST_CASE(execute_error_num_params) { // Setup auto stmt = statement_builder().id(1).num_params(2).build(); const auto params = make_fv_arr("test", nullptr, 42); // too many params - fixture fix(any_execution_request(stmt, params)); + execute_fixture fix(any_execution_request(stmt, params)); // Run the algo. Nothing should be written to the server algo_test().check(fix, client_errc::wrong_num_params); @@ -189,7 +251,7 @@ BOOST_AUTO_TEST_CASE(error_num_params) } // Tests error on write, while reading head and while reading rows (error spotcheck) -BOOST_AUTO_TEST_CASE(error_network_error) +BOOST_AUTO_TEST_CASE(execute_error_network_error) { algo_test() .expect_write(create_frame(0, serialized_select_1)) @@ -198,7 +260,7 @@ BOOST_AUTO_TEST_CASE(error_network_error) .expect_read(create_text_row_message(3, 42)) .expect_read(create_text_row_message(4, 43)) .expect_read(create_eof_frame(5, ok_builder().affected_rows(10u).info("1st").build())) - .check_network_errors(); + .check_network_errors(); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/message_writer.cpp b/test/unit/test/sansio/message_writer.cpp deleted file mode 100644 index c1e1400e..00000000 --- a/test/unit/test/sansio/message_writer.cpp +++ /dev/null @@ -1,323 +0,0 @@ -// -// Copyright (c) 2019-2024 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 "test_common/assert_buffer_equals.hpp" -#include "test_common/buffer_concat.hpp" -#include "test_unit/create_frame.hpp" -#include "test_unit/mock_message.hpp" - -using namespace boost::mysql::detail; -using namespace boost::mysql::test; -using boost::span; - -namespace { - -BOOST_AUTO_TEST_SUITE(test_message_writer) - -BOOST_AUTO_TEST_CASE(regular_message) -{ - message_writer writer; - const std::vector msg_body{0x01, 0x02, 0x03}; - std::uint8_t seqnum = 2; - - // Operation start - writer.prepare_write(mock_message{msg_body}, seqnum); - BOOST_TEST(!writer.done()); - - // First (and only) chunk - auto chunk = writer.current_chunk(); - auto expected = create_frame(2, msg_body); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful - writer.resume(7); - BOOST_TEST(seqnum == 3u); - BOOST_TEST(writer.done()); -} - -BOOST_AUTO_TEST_CASE(short_writes) -{ - message_writer writer; - const std::vector msg_body{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; - std::uint8_t seqnum = 2; - - // Operation start - writer.prepare_write(mock_message{msg_body}, seqnum); - - // First chunk - auto chunk = writer.current_chunk(); - auto expected = create_frame(2, msg_body); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // Write signals partial write - writer.resume(3); - BOOST_TEST(seqnum == 3u); - BOOST_TEST(!writer.done()); - - // Remaining of the chunk - chunk = writer.current_chunk(); - span expected_buff(expected.data() + 3, 7); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected_buff); - - // Another partial write - writer.resume(2); - BOOST_TEST(seqnum == 3u); - BOOST_TEST(!writer.done()); - - // Remaining of the chunk - chunk = writer.current_chunk(); - expected_buff = span(expected.data() + 5, 5); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected_buff); - - // Zero bytes partial writes work correctly - BOOST_TEST(seqnum == 3u); - BOOST_TEST(!writer.done()); - - // Remaining of the chunk - chunk = writer.current_chunk(); - expected_buff = span(expected.data() + 5, 5); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected_buff); - - // Final bytes - writer.resume(5); - BOOST_TEST(seqnum == 3u); - BOOST_TEST(writer.done()); -} - -BOOST_AUTO_TEST_CASE(several_messages) -{ - message_writer writer; - std::vector msg_1{0x01, 0x02, 0x04}; - std::vector msg_2{0x04, 0x05, 0x06, 0x09, 0xff}; - std::vector msg_3{0x02, 0xab}; - std::uint8_t seqnum_1 = 2; - std::uint8_t seqnum_2 = 42; - std::uint8_t seqnum_3 = 21; - - // Operation start for message 1 - writer.prepare_write(mock_message{msg_1}, seqnum_1); - BOOST_TEST(!writer.done()); - - // Chunk 1 - auto chunk = writer.current_chunk(); - auto expected = create_frame(2, msg_1); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 1 - writer.resume(7); - BOOST_TEST(seqnum_1 == 3u); - BOOST_TEST(writer.done()); - - // Operation start for message 2 - writer.prepare_write(mock_message{msg_2}, seqnum_2); - BOOST_TEST(!writer.done()); - - // Chunk 2 - chunk = writer.current_chunk(); - expected = create_frame(42, msg_2); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 2 - writer.resume(9); - BOOST_TEST(seqnum_2 == 43u); - BOOST_TEST(writer.done()); - - // Operation start for message 3 - writer.prepare_write(mock_message{msg_3}, seqnum_3); - BOOST_TEST(!writer.done()); - - // Chunk 3 - chunk = writer.current_chunk(); - expected = create_frame(21, msg_3); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 3 - writer.resume(6); - BOOST_TEST(seqnum_3 == 22u); - BOOST_TEST(writer.done()); -} - -// After a message has been partially written, we can -// prepare a write and resume operation normally. This can -// happen after a tiemout ellapses and a reconnection occurs. -BOOST_AUTO_TEST_CASE(message_half_written) -{ - message_writer writer; - std::vector msg_1{0x01, 0x02, 0x04}; - std::vector msg_2{0x04, 0x05, 0x06, 0x09, 0xff}; - std::uint8_t seqnum_1 = 2; - std::uint8_t seqnum_2 = 42; - - // Operation start for message 1 - writer.prepare_write(mock_message{msg_1}, seqnum_1); - BOOST_TEST(!writer.done()); - - // Chunk 1 - auto chunk = writer.current_chunk(); - auto expected = create_frame(2, msg_1); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // Remaining of message 1 is never written. - // Operation start for message 2 - writer.prepare_write(mock_message{msg_2}, seqnum_2); - BOOST_TEST(!writer.done()); - - // Chunk 2 - chunk = writer.current_chunk(); - expected = create_frame(42, msg_2); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 2 - writer.resume(9); - BOOST_TEST(seqnum_2 == 43u); - BOOST_TEST(writer.done()); -} - -BOOST_AUTO_TEST_CASE(pipelined_write) -{ - message_writer writer; - std::vector msg_1{0x01, 0x02, 0x04, 0x05}; - std::vector msg_2{0x04, 0x05, 0x06, 0x09, 0xff}; - std::uint8_t seqnum_1 = 2; - std::uint8_t seqnum_2 = 42; - - // Prepare a pipelined write. This is currently limited - // to two messages which are shorter than the max frame size. - writer.prepare_pipelined_write(mock_message{msg_1}, seqnum_1, mock_message{msg_2}, seqnum_2); - BOOST_TEST(!writer.done()); - - // Chunk 1 - auto chunk = writer.current_chunk(); - auto expected = buffer_builder().add(create_frame(2, msg_1)).add(create_frame(42, msg_2)).build(); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // Some bytes acknowledged - writer.resume(5); - chunk = writer.current_chunk(); - auto expected_view = span(expected).subspan(5); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected_view); - BOOST_TEST(!writer.done()); - - // More bytes acknowledged - writer.resume(10); - chunk = writer.current_chunk(); - expected_view = span(expected).subspan(15); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected_view); - BOOST_TEST(!writer.done()); - - // Final bytes acknowledged - writer.resume(2); - BOOST_TEST(writer.done()); - BOOST_TEST(seqnum_1 == 3u); - BOOST_TEST(seqnum_2 == 43u); -} - -// Interleaving pipelined writes with regular writes works -BOOST_AUTO_TEST_CASE(pipelined_write_interleaved) -{ - message_writer writer; - std::vector msg_1{0x01, 0x02, 0x04}; - std::vector msg_2{0x04, 0x05, 0x06, 0x09, 0xff}; - std::vector msg_3{0x02, 0xab}; - std::vector msg_4{0x05}; - std::uint8_t seqnum_1 = 2; - std::uint8_t seqnum_2 = 42; - std::uint8_t seqnum_3 = 21; - std::uint8_t seqnum_4 = 100; - - // Operation start for message 1 - writer.prepare_write(mock_message{msg_1}, seqnum_1); - BOOST_TEST(!writer.done()); - - // Chunk 1 - auto chunk = writer.current_chunk(); - auto expected = create_frame(2, msg_1); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 1 - writer.resume(7); - BOOST_TEST(seqnum_1 == 3u); - BOOST_TEST(writer.done()); - - // Operation start for messages 2 and 3 - writer.prepare_pipelined_write(mock_message{msg_2}, seqnum_2, mock_message{msg_3}, seqnum_3); - BOOST_TEST(!writer.done()); - - // Chunk 2 - chunk = writer.current_chunk(); - expected = concat_copy(create_frame(42, msg_2), create_frame(21, msg_3)); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful messages 2 and 3 - writer.resume(15); - BOOST_TEST(seqnum_2 == 43u); - BOOST_TEST(seqnum_3 == 22u); - BOOST_TEST(writer.done()); - - // Operation start for message 4 - writer.prepare_write(mock_message{msg_4}, seqnum_4); - BOOST_TEST(!writer.done()); - - // Chunk 4 - chunk = writer.current_chunk(); - expected = create_frame(100, msg_4); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 4 - writer.resume(5); - BOOST_TEST(seqnum_4 == 101u); - BOOST_TEST(writer.done()); -} - -// Leaving a message half written is not a problem -BOOST_AUTO_TEST_CASE(pipelined_message_half_written) -{ - message_writer writer; - std::vector msg_1{0x01, 0x02, 0x04}; - std::vector msg_2{0x04, 0x05, 0x06, 0x09, 0xff}; - std::vector msg_3{0x02, 0xab}; - std::uint8_t seqnum_1 = 2; - std::uint8_t seqnum_2 = 42; - std::uint8_t seqnum_3 = 21; - - // Operation start for messages 1 and 2 - writer.prepare_pipelined_write(mock_message{msg_1}, seqnum_1, mock_message{msg_2}, seqnum_2); - BOOST_TEST(!writer.done()); - auto chunk = writer.current_chunk(); - auto expected = concat_copy(create_frame(2, msg_1), create_frame(42, msg_2)); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // Part of this pipeline is written - writer.resume(8); - BOOST_TEST(!writer.done()); - - // The rest is never written - // Operation start for message 3 - writer.prepare_write(mock_message{msg_3}, seqnum_3); - BOOST_TEST(!writer.done()); - - // Chunk - chunk = writer.current_chunk(); - expected = create_frame(21, msg_3); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(chunk, expected); - - // On write successful message 3 - writer.resume(6); - BOOST_TEST(seqnum_3 == 22u); - BOOST_TEST(writer.done()); -} - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace diff --git a/test/unit/test/sansio/ping.cpp b/test/unit/test/sansio/ping.cpp index 7321bf54..ced5b623 100644 --- a/test/unit/test/sansio/ping.cpp +++ b/test/unit/test/sansio/ping.cpp @@ -11,16 +11,13 @@ #include #include +#include #include -#include - -#include "test_common/assert_buffer_equals.hpp" #include "test_common/printing.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" @@ -29,15 +26,81 @@ using namespace boost::mysql; BOOST_AUTO_TEST_SUITE(test_ping) -struct fixture : algo_fixture_base +// +// read_ping_response_algo +// +struct read_response_fixture : algo_fixture_base { - detail::ping_algo algo{st, {&diag}}; + detail::read_ping_response_algo algo{&diag, 57}; + + // Clearing diagnostics is not this algorithm's responsibility + read_response_fixture() : algo_fixture_base(diagnostics()) {} }; -BOOST_AUTO_TEST_CASE(success) +BOOST_AUTO_TEST_CASE(read_response_success) { // Setup - fixture fix; + read_response_fixture fix; + + // Run the test + algo_test() + .expect_read(create_ok_frame(57, ok_builder().build())) // OK response + .check(fix); + + // The OK packet was processed correctly + BOOST_TEST(fix.st.backslash_escapes); +} + +BOOST_AUTO_TEST_CASE(read_response_success_no_backslash_escapes) +{ + // Setup + read_response_fixture fix; + + // Run the test + algo_test() + .expect_read(create_ok_frame(57, ok_builder().no_backslash_escapes(true).build())) // OK response + .check(fix); + + // The OK packet was processed correctly + BOOST_TEST(!fix.st.backslash_escapes); +} + +BOOST_AUTO_TEST_CASE(read_response_error_network) +{ + algo_test() + .expect_read(create_ok_frame(57, ok_builder().build())) + .check_network_errors(); +} + +BOOST_AUTO_TEST_CASE(read_response_error_packet) +{ + // Setup + read_response_fixture fix; + + // Run the test + algo_test() + .expect_read(err_builder() + .seqnum(57) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) // Error response + .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); +} + +// +// setup_ping_pipeline: running a pipeline with these parameters +// has the intended effect +// + +struct ping_fixture : algo_fixture_base +{ + detail::run_pipeline_algo algo{detail::setup_ping_pipeline(st, {&diag})}; +}; + +BOOST_AUTO_TEST_CASE(ping_success) +{ + // Setup + ping_fixture fix; // Run the test algo_test() @@ -49,34 +112,19 @@ BOOST_AUTO_TEST_CASE(success) BOOST_TEST(fix.st.backslash_escapes); } -BOOST_AUTO_TEST_CASE(success_no_backslash_escapes) -{ - // Setup - fixture fix; - - // Run the test - algo_test() - .expect_write({0x01, 0x00, 0x00, 0x00, 0x0e}) // ping request - .expect_read(create_ok_frame(1, ok_builder().no_backslash_escapes(true).build())) // OK response - .check(fix); - - // The OK packet was processed correctly - BOOST_TEST(!fix.st.backslash_escapes); -} - -BOOST_AUTO_TEST_CASE(error_network) +BOOST_AUTO_TEST_CASE(ping_error_network) { // Check for net errors for each read/write algo_test() .expect_write({0x01, 0x00, 0x00, 0x00, 0x0e}) .expect_read(create_ok_frame(1, ok_builder().build())) - .check_network_errors(); + .check_network_errors(); } -BOOST_AUTO_TEST_CASE(error_response) +BOOST_AUTO_TEST_CASE(ping_error_response) { // Setup - fixture fix; + ping_fixture fix; // Run the test algo_test() diff --git a/test/unit/test/sansio/prepare_statement.cpp b/test/unit/test/sansio/prepare_statement.cpp new file mode 100644 index 00000000..747cfc19 --- /dev/null +++ b/test/unit/test/sansio/prepare_statement.cpp @@ -0,0 +1,216 @@ +// +// Copyright (c) 2019-2024 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_unit/algo_test.hpp" +#include "test_unit/create_coldef_frame.hpp" +#include "test_unit/create_err.hpp" +#include "test_unit/create_frame.hpp" +#include "test_unit/create_meta.hpp" +#include "test_unit/create_prepare_statement_response.hpp" +#include "test_unit/create_query_frame.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; + +BOOST_AUTO_TEST_SUITE(test_prepare_statement) + +// +// read_prepare_statement_response_algo +// +struct read_response_fixture : algo_fixture_base +{ + detail::read_prepare_statement_response_algo algo{&diag, 19}; + + // Clearing diagnostics is not this algorithm's responsibility + read_response_fixture() : algo_fixture_base(diagnostics()) {} + + statement result() const { return algo.result(st); } +}; + +BOOST_AUTO_TEST_CASE(read_response_success) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(1).num_columns(1).num_params(2).build()) + .expect_read(create_coldef_frame(20, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(21, meta_builder().name("other").build_coldef())) + .expect_read(create_coldef_frame(22, meta_builder().name("final").build_coldef())) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 1u); + BOOST_TEST(stmt.num_params() == 2u); +} + +BOOST_AUTO_TEST_CASE(read_response_success_0cols) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(5).num_columns(0).num_params(1).build()) + .expect_read(create_coldef_frame(20, meta_builder().name("abc").build_coldef())) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 5u); + BOOST_TEST(stmt.num_params() == 1u); +} + +BOOST_AUTO_TEST_CASE(read_response_success_0params) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(214).num_columns(2).num_params(0).build()) + .expect_read(create_coldef_frame(20, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(21, meta_builder().name("defff").build_coldef())) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 214u); + BOOST_TEST(stmt.num_params() == 0u); +} + +BOOST_AUTO_TEST_CASE(read_response_success_0cols_0params) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(98).num_columns(0).num_params(0).build()) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 98u); + BOOST_TEST(stmt.num_params() == 0u); +} + +// A statement_id == 0 doesn't cause trouble +BOOST_AUTO_TEST_CASE(read_response_success_0id) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(0).num_columns(0).num_params(1).build()) + .expect_read(create_coldef_frame(20, meta_builder().name("abc").build_coldef())) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 0u); + BOOST_TEST(stmt.num_params() == 1u); +} + +BOOST_AUTO_TEST_CASE(read_response_error_network) +{ + algo_test() + .expect_read(prepare_stmt_response_builder().seqnum(19).id(1).num_columns(1).num_params(2).build()) + .expect_read(create_coldef_frame(20, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(21, meta_builder().name("other").build_coldef())) + .expect_read(create_coldef_frame(22, meta_builder().name("final").build_coldef())) + .check_network_errors(); +} + +BOOST_AUTO_TEST_CASE(read_response_error_packet) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test() + .expect_read(err_builder() + .seqnum(19) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) + .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); +} + +// +// prepare_statement_algo +// +struct prepare_fixture : algo_fixture_base +{ + detail::prepare_statement_algo algo{ + {&diag, "SELECT 1"} + }; + + statement result() const { return algo.result(st); } +}; + +BOOST_AUTO_TEST_CASE(prepare_success) +{ + // Setup + prepare_fixture fix; + + // Run the algo + algo_test() + .expect_write(create_prepare_statement_frame(0, "SELECT 1")) + .expect_read(prepare_stmt_response_builder().seqnum(1).id(29).num_columns(0).num_params(2).build()) + .expect_read(create_coldef_frame(2, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(3, meta_builder().name("other").build_coldef())) + .check(fix); + + // The statement was created successfully + auto stmt = fix.result(); + BOOST_TEST(stmt.id() == 29u); + BOOST_TEST(stmt.num_params() == 2u); +} + +BOOST_AUTO_TEST_CASE(prepare_network_error) +{ + // This covers errors in the request and the response + algo_test() + .expect_write(create_prepare_statement_frame(0, "SELECT 1")) + .expect_read(prepare_stmt_response_builder().seqnum(1).id(29).num_columns(0).num_params(1).build()) + .expect_read(create_coldef_frame(2, meta_builder().name("abc").build_coldef())) + .check_network_errors(); +} + +// Spotcheck: an error while reading the response is propagated correctly +BOOST_AUTO_TEST_CASE(prepare_error_packet) +{ + // Setup + prepare_fixture fix; + + // Run the algo + algo_test() + .expect_write(create_prepare_statement_frame(0, "SELECT 1")) + .expect_read(err_builder() + .seqnum(1) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) + .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/read_resultset_head.cpp b/test/unit/test/sansio/read_resultset_head.cpp index c9cb48b0..ddd3b581 100644 --- a/test/unit/test/sansio/read_resultset_head.cpp +++ b/test/unit/test/sansio/read_resultset_head.cpp @@ -39,7 +39,6 @@ struct fixture : algo_fixture_base { mock_execution_processor proc; detail::read_resultset_head_algo algo{ - st, {&diag, &proc} }; @@ -255,4 +254,25 @@ BOOST_AUTO_TEST_CASE(error_on_meta) fix.proc.num_calls().on_num_meta(1).on_meta(1).validate(); } +BOOST_AUTO_TEST_CASE(reset) +{ + // Setup + fixture fix; + + // Run the algo once + algo_test() + .expect_read(create_frame(1, {0x01})) // 1 metadata follows + .expect_read(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef())) + .check(fix); + fix.proc.num_calls().on_num_meta(1).on_meta(1).validate(); + + // Reset. Place the processor into a state where we can read head again + fix.algo.reset(); + add_ok(fix.proc, ok_builder().more_results(true).build()); + + // Run it again + algo_test().expect_read(create_ok_frame(3, ok_builder().build())).check(fix); + fix.proc.num_calls().on_num_meta(1).on_meta(1).on_row_ok_packet(1).on_head_ok_packet(1).validate(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/read_some_rows.cpp b/test/unit/test/sansio/read_some_rows.cpp index eb83f4f1..109e90ae 100644 --- a/test/unit/test/sansio/read_some_rows.cpp +++ b/test/unit/test/sansio/read_some_rows.cpp @@ -43,7 +43,6 @@ struct fixture : algo_fixture_base mock_execution_processor proc; std::array storage; detail::read_some_rows_algo algo{ - st, {&diag, &proc, ref()} }; @@ -66,6 +65,8 @@ struct fixture : algo_fixture_base for (std::size_t i = 0; i < num_rows; ++i) BOOST_TEST(proc.refs()[i].offset() == i); } + + std::size_t result() const { return algo.result(st); } }; BOOST_AUTO_TEST_CASE(eof) @@ -80,7 +81,7 @@ BOOST_AUTO_TEST_CASE(eof) ) .check(fix); - BOOST_TEST(fix.algo.result() == 0u); // num read rows + BOOST_TEST(fix.result() == 0u); // num read rows BOOST_TEST(fix.proc.is_reading_head()); BOOST_TEST(fix.proc.affected_rows() == 1u); BOOST_TEST(fix.proc.info() == "1st"); @@ -97,7 +98,7 @@ BOOST_AUTO_TEST_CASE(eof_no_backslash_escapes) .expect_read(create_eof_frame(42, ok_builder().no_backslash_escapes(true).more_results(true).build())) .check(fix); - BOOST_TEST(fix.algo.result() == 0u); // num read rows + BOOST_TEST(fix.result() == 0u); // num read rows BOOST_TEST(fix.proc.is_reading_head()); BOOST_TEST(!fix.st.backslash_escapes); } @@ -116,7 +117,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 2u); // num read rows + BOOST_TEST(fix.result() == 2u); // num read rows BOOST_TEST(fix.proc.is_reading_rows()); fix.validate_refs(2); fix.proc.num_calls() @@ -147,7 +148,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows_eof) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 2u); // num read rows + BOOST_TEST(fix.result() == 2u); // num read rows BOOST_TEST_REQUIRE(fix.proc.is_reading_head()); BOOST_TEST(fix.proc.affected_rows() == 1u); BOOST_TEST(fix.proc.info() == "1st"); @@ -182,7 +183,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows_eof_multiresult) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 1u); // num read rows + BOOST_TEST(fix.result() == 1u); // num read rows BOOST_TEST_REQUIRE(fix.proc.is_reading_head()); BOOST_TEST(fix.proc.affected_rows() == 1u); BOOST_TEST(fix.proc.info() == "1st"); @@ -214,7 +215,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows_out_of_span_space) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 3u); // num read rows + BOOST_TEST(fix.result() == 3u); // num read rows fix.validate_refs(3); BOOST_TEST(fix.proc.is_reading_rows()); fix.proc.num_calls() @@ -242,7 +243,7 @@ BOOST_AUTO_TEST_CASE(successive_calls_keep_parsing_state) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 2u); // num read rows + BOOST_TEST(fix.result() == 2u); // num read rows fix.validate_refs(2); BOOST_TEST(fix.proc.is_reading_rows()); fix.proc.num_calls() @@ -254,14 +255,14 @@ BOOST_AUTO_TEST_CASE(successive_calls_keep_parsing_state) .validate(); // Run the algo again - fix.algo = detail::read_some_rows_algo(fix.st, fix.algo.params()); + fix.algo.reset(); fix.diag = create_server_diag("Diagnostics not cleared"); algo_test() .expect_read(buffer_builder().add(boost::span(eof).subspan(6)).build()) .check(fix); // Validate - BOOST_TEST(fix.algo.result() == 0u); // num read rows + BOOST_TEST(fix.result() == 0u); // num read rows BOOST_TEST(fix.proc.is_complete()); fix.proc.num_calls() .on_num_meta(1) @@ -286,7 +287,7 @@ BOOST_AUTO_TEST_CASE(state_complete) algo_test().check(fix); // Validate - BOOST_TEST(fix.algo.result() == 0u); // num read rows + BOOST_TEST(fix.result() == 0u); // num read rows BOOST_TEST(fix.proc.is_complete()); fix.proc.num_calls().on_num_meta(1).on_meta(1).on_row_ok_packet(1).validate(); } @@ -301,7 +302,7 @@ BOOST_AUTO_TEST_CASE(state_reading_head) algo_test().check(fix); // Validate - BOOST_TEST(fix.algo.result() == 0u); // num read rows + BOOST_TEST(fix.result() == 0u); // num read rows BOOST_TEST(fix.proc.is_reading_head()); fix.proc.num_calls().on_num_meta(1).on_meta(1).on_row_ok_packet(1).validate(); } @@ -376,4 +377,43 @@ BOOST_AUTO_TEST_CASE(error_deserialize_row_message) fix.proc.num_calls().on_num_meta(1).on_meta(1).on_row_batch_start(1).validate(); } +BOOST_AUTO_TEST_CASE(reset) +{ + // Setup + fixture fix; + + // Run the algo. Read a row + algo_test().expect_read(create_text_row_message(42, "abc")).check(fix); + + // Validate + BOOST_TEST(fix.result() == 1u); // num read rows + BOOST_TEST(fix.proc.is_reading_rows()); + fix.validate_refs(1); + fix.proc.num_calls() + .on_num_meta(1) + .on_meta(1) + .on_row_batch_start(1) + .on_row(1) + .on_row_batch_finish(1) + .validate(); + + // Reset + fix.algo.reset(); + + // Run the algo again. Read an OK packet + algo_test().expect_read(create_eof_frame(43, ok_builder().build())).check(fix); + + // Check + BOOST_TEST(fix.result() == 0u); // num read rows + BOOST_TEST(fix.proc.is_complete()); + fix.proc.num_calls() + .on_num_meta(1) + .on_meta(1) + .on_row_batch_start(2) + .on_row(1) + .on_row_batch_finish(2) + .on_row_ok_packet(1) + .validate(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/read_some_rows_dynamic.cpp b/test/unit/test/sansio/read_some_rows_dynamic.cpp index 8cd2f8fb..e537779d 100644 --- a/test/unit/test/sansio/read_some_rows_dynamic.cpp +++ b/test/unit/test/sansio/read_some_rows_dynamic.cpp @@ -7,6 +7,7 @@ #include #include +#include #include @@ -34,7 +35,6 @@ struct fixture : algo_fixture_base { execution_state_impl exec_st; detail::read_some_rows_dynamic_algo algo{ - st, {&diag, &exec_st} }; @@ -47,6 +47,8 @@ struct fixture : algo_fixture_base // Put something in shared_fields, simulating a previous read st.shared_fields.push_back(field_view("prev")); } + + rows_view result() const { return algo.result(st); } }; BOOST_AUTO_TEST_CASE(eof) @@ -59,7 +61,7 @@ BOOST_AUTO_TEST_CASE(eof) .expect_read(create_eof_frame(42, ok_builder().affected_rows(1).info("1st").build())) .check(fix); - BOOST_TEST(fix.algo.result() == makerows(1)); + BOOST_TEST(fix.result() == makerows(1)); BOOST_TEST_REQUIRE(fix.exec_st.is_complete()); BOOST_TEST(fix.exec_st.get_affected_rows() == 1u); BOOST_TEST(fix.exec_st.get_info() == "1st"); @@ -79,7 +81,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows) .check(fix); // Check - BOOST_TEST(fix.algo.result() == makerows(1, "abc", "von")); + BOOST_TEST(fix.result() == makerows(1, "abc", "von")); BOOST_TEST(fix.exec_st.is_reading_rows()); } @@ -98,7 +100,7 @@ BOOST_AUTO_TEST_CASE(batch_with_rows_eof) .check(fix); // Check - BOOST_TEST(fix.algo.result() == makerows(1, "abc", "von")); + BOOST_TEST(fix.result() == makerows(1, "abc", "von")); BOOST_TEST_REQUIRE(fix.exec_st.is_complete()); BOOST_TEST(fix.exec_st.get_affected_rows() == 1u); BOOST_TEST(fix.exec_st.get_info() == "1st"); diff --git a/test/unit/test/sansio/reset_connection.cpp b/test/unit/test/sansio/reset_connection.cpp index 56a3465e..51142869 100644 --- a/test/unit/test/sansio/reset_connection.cpp +++ b/test/unit/test/sansio/reset_connection.cpp @@ -12,35 +12,100 @@ #include #include +#include #include -#include "test_common/assert_buffer_equals.hpp" -#include "test_common/buffer_concat.hpp" #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" -#include "test_unit/create_query_frame.hpp" using namespace boost::mysql::test; using namespace boost::mysql; BOOST_AUTO_TEST_SUITE(test_reset_connection) -struct fixture : algo_fixture_base +// +// read_reset_connection_response_algo +// +struct read_response_fixture : algo_fixture_base { - detail::reset_connection_algo algo; + detail::read_reset_connection_response_algo algo{&diag, 11}; - fixture(character_set charset = {}) : algo(st, {&diag, charset}) {} + // Clearing diagnostics is not this algorithm's responsibility + read_response_fixture() : algo_fixture_base(diagnostics()) {} +}; + +BOOST_AUTO_TEST_CASE(read_response_success) +{ + // Setup + read_response_fixture fix; + fix.st.current_charset = utf8mb4_charset; + + // Run the algo + algo_test().expect_read(create_ok_frame(11, ok_builder().build())).check(fix); + + // 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(read_response_success_no_backslash_escapes) +{ + // Setup + read_response_fixture fix; + + // Run the algo + algo_test().expect_read(create_ok_frame(11, ok_builder().no_backslash_escapes(true).build())).check(fix); + + // 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(read_response_error_network) +{ + algo_test() + .expect_read(create_ok_frame(11, ok_builder().build())) + .check_network_errors(); +} + +BOOST_AUTO_TEST_CASE(read_response_error_packet) +{ + // Setup + read_response_fixture fix; + fix.st.current_charset = utf8mb4_charset; + + // Run the algo + algo_test() + .expect_read(err_builder() + .seqnum(11) + .code(common_server_errc::er_bad_db_error) + .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 == "utf8mb4"); +} + +// +// setup_reset_connection_pipeline: running a pipeline with these parameters +// has the intended effect +// +struct reset_conn_fixture : algo_fixture_base +{ + detail::run_pipeline_algo algo{detail::setup_reset_connection_pipeline(st, {&diag})}; }; BOOST_AUTO_TEST_CASE(success) { // Setup - fixture fix; + reset_conn_fixture fix; fix.st.current_charset = utf8mb4_charset; // Run the algo @@ -54,36 +119,19 @@ BOOST_AUTO_TEST_CASE(success) BOOST_TEST(fix.st.charset_ptr() == nullptr); } -BOOST_AUTO_TEST_CASE(success_no_backslash_escapes) -{ - // Setup - fixture fix; - - // Run the algo - algo_test() - .expect_write(create_frame(0, {0x1f})) - .expect_read(create_ok_frame(1, ok_builder().no_backslash_escapes(true).build())) - .check(fix); - - // 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) +BOOST_AUTO_TEST_CASE(reset_conn_error_network) { // This covers errors in read and write algo_test() .expect_write(create_frame(0, {0x1f})) .expect_read(create_ok_frame(1, ok_builder().build())) - .check_network_errors(); + .check_network_errors(); } -BOOST_AUTO_TEST_CASE(error_response) +BOOST_AUTO_TEST_CASE(reset_conn_error_response) { // Setup - fixture fix; + reset_conn_fixture fix; fix.st.current_charset = utf8mb4_charset; // Run the algo @@ -100,114 +148,4 @@ BOOST_AUTO_TEST_CASE(error_response) BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); } -BOOST_AUTO_TEST_CASE(charset_success) -{ - // Setup - fixture fix(ascii_charset); - fix.st.current_charset = utf8mb4_charset; - - // Run the algo - algo_test() - .expect_write(concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SET NAMES 'ascii'"))) - .expect_read(create_ok_frame(1, ok_builder().build())) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check(fix); - - // The OK packets were processed correctly. The charset is set - BOOST_TEST(fix.st.backslash_escapes); - BOOST_TEST(fix.st.charset_ptr()->name == "ascii"); -} - -BOOST_AUTO_TEST_CASE(charset_error_network) -{ - struct charset_fixture : fixture - { - charset_fixture() : fixture(ascii_charset) {} - }; - - // This covers errors in read and write - algo_test() - .expect_write(concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SET NAMES 'ascii'"))) - .expect_read(create_ok_frame(1, ok_builder().build())) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check_network_errors(); -} - -BOOST_AUTO_TEST_CASE(charset_reset_error_response) -{ - // Setup - fixture fix(ascii_charset); - fix.st.current_charset = utf8mb4_charset; - - // Run the algo - algo_test() - .expect_write(concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SET NAMES 'ascii'"))) - .expect_read( - err_builder().seqnum(1).code(common_server_errc::er_cant_create_db).message("abc").build_frame() - ) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check(fix, common_server_errc::er_cant_create_db, create_server_diag("abc")); - - // Both packets are read, even if the first one is an error. The character set is updated because the - // second packet is an OK - BOOST_TEST(fix.st.backslash_escapes); - BOOST_TEST(fix.st.charset_ptr()->name == "ascii"); -} - -BOOST_AUTO_TEST_CASE(charset_set_names_error_response) -{ - // Setup - fixture fix(ascii_charset); - fix.st.current_charset = utf8mb4_charset; - - // Run the algo - algo_test() - .expect_write(concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SET NAMES 'ascii'"))) - .expect_read(create_ok_frame(1, ok_builder().build())) - .expect_read( - err_builder().seqnum(1).code(common_server_errc::er_cant_create_db).message("abc").build_frame() - ) - .check(fix, common_server_errc::er_cant_create_db, create_server_diag("abc")); - - // Both packets are read. The character set is reset - BOOST_TEST(fix.st.backslash_escapes); - BOOST_TEST(fix.st.charset_ptr() == nullptr); -} - -BOOST_AUTO_TEST_CASE(charset_all_error_responses) -{ - // Setup - fixture fix(ascii_charset); - fix.st.current_charset = utf8mb4_charset; - - // Run the algo - algo_test() - .expect_write(concat_copy(create_frame(0, {0x1f}), create_query_frame(0, "SET NAMES 'ascii'"))) - .expect_read(err_builder() - .seqnum(1) - .code(common_server_errc::er_autoinc_read_failed) - .message("hello") - .build_frame()) - .expect_read( - err_builder().seqnum(1).code(common_server_errc::er_cant_create_db).message("abc").build_frame() - ) - .check(fix, common_server_errc::er_autoinc_read_failed, create_server_diag("hello")); - - // Both packets are read. The character is not reset - BOOST_TEST(fix.st.backslash_escapes); - BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); -} - -BOOST_AUTO_TEST_CASE(charset_error_nonascii_charset_name) -{ - // Setup - fixture fix({"lat\xc3\xadn", ascii_charset.next_char}); - - // Run the algo - algo_test().check(fix, client_errc::invalid_encoding); - - // The current character set was not updated - BOOST_TEST(fix.st.charset_ptr() == nullptr); -} - BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/run_pipeline.cpp b/test/unit/test/sansio/run_pipeline.cpp new file mode 100644 index 00000000..90b1d4ce --- /dev/null +++ b/test/unit/test/sansio/run_pipeline.cpp @@ -0,0 +1,603 @@ +// +// Copyright (c) 2019-2024 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 + +#include +#include +#include + +#include +#include +#include +#include + +#include "test_common/buffer_concat.hpp" +#include "test_common/create_diagnostics.hpp" +#include "test_unit/algo_test.hpp" +#include "test_unit/create_coldef_frame.hpp" +#include "test_unit/create_err.hpp" +#include "test_unit/create_frame.hpp" +#include "test_unit/create_meta.hpp" +#include "test_unit/create_ok.hpp" +#include "test_unit/create_ok_frame.hpp" +#include "test_unit/create_prepare_statement_response.hpp" +#include "test_unit/create_row_message.hpp" +#include "test_unit/mock_execution_processor.hpp" +#include "test_unit/printing.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; +namespace asio = boost::asio; +using boost::span; +using boost::test_tools::per_element; +using detail::pipeline_request_stage; +using detail::pipeline_stage_kind; +using detail::resultset_encoding; + +namespace boost { +namespace mysql { +namespace test { + +struct mock_pipeline_response +{ + struct item + { + mock_execution_processor proc; + statement stmt; + errcode_with_diagnostics err; + }; + + std::size_t setup_num_calls{0u}; + span setup_args; + std::vector items; +}; + +} // namespace test + +namespace detail { + +template <> +struct pipeline_response_traits +{ + using response_type = test::mock_pipeline_response; + + static void setup(response_type& self, span request) + { + ++self.setup_num_calls; + self.setup_args = request; + self.items.resize(request.size()); + } + + static execution_processor& get_processor(response_type& self, std::size_t idx) + { + // This function is only defined for execute stages + BOOST_TEST_REQUIRE(idx < self.items.size()); + BOOST_TEST(self.setup_args[idx].kind == pipeline_stage_kind::execute); + return self.items[idx].proc; + } + + static void set_result(response_type& self, std::size_t idx, statement stmt) + { + // This function is only defined for prepare statement stages + BOOST_TEST_REQUIRE(idx < self.items.size()); + BOOST_TEST(self.setup_args[idx].kind == pipeline_stage_kind::prepare_statement); + self.items[idx].stmt = stmt; + } + + static void set_error(response_type& self, std::size_t idx, error_code ec, diagnostics&& diag) + { + BOOST_TEST_REQUIRE(idx < self.items.size()); + self.items[idx].err = {ec, std::move(diag)}; + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +BOOST_AUTO_TEST_SUITE(test_run_pipeline) + +const std::vector mock_request{1, 2, 3, 4, 5, 6, 7, 9, 21}; + +struct fixture : algo_fixture_base +{ + detail::run_pipeline_algo algo; + mock_pipeline_response resp; + + fixture(span stages, span req_buffer = mock_request) + : algo({ + &diag, + detail::pipeline_request_view{req_buffer, stages}, + detail::pipeline_response_ref(resp) + }) + { + } + + // Verify that the stages we passed match the ones used in setup + void check_setup(span stages) + { + BOOST_TEST(resp.setup_num_calls == 1u); + BOOST_TEST(resp.setup_args == stages, per_element()); + } + + // Verify that all stages succeeded + void check_all_stages_succeeded() + { + for (const auto& item : resp.items) + BOOST_TEST(item.err == errcode_with_diagnostics{}); + } + + // Verify that a certain step failed + void check_stage_error(std::size_t i, const errcode_with_diagnostics& expected) + { + BOOST_TEST(resp.items.at(i).err == expected); + } +}; + +// All stage kinds work properly +BOOST_AUTO_TEST_CASE(execute_success) +{ + // Setup. Each step has a different encoding + const std::array stages{ + { + {pipeline_stage_kind::execute, 42u, resultset_encoding::binary}, + {pipeline_stage_kind::execute, 11u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read(create_ok_frame(42, ok_builder().info("1st").build())) // 1st op ok + .expect_read(create_frame(11, {0x01})) // 2nd op OK, 1 column + .expect_read(create_coldef_frame(12, meta_builder().type(column_type::tinyint).build_coldef())) + .expect_read(buffer_builder() + .add(create_text_row_message(13, 42)) + .add(create_text_row_message(14, 43)) + .add(create_eof_frame(15, ok_builder().info("2nd").build())) + .build()) + .check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // Check each processor calls + auto& proc0 = fix.resp.items.at(0).proc; + proc0.num_calls().reset(1).on_head_ok_packet(1).validate(); + BOOST_TEST(proc0.info() == "1st"); + BOOST_TEST(proc0.encoding() == resultset_encoding::binary); + + auto& proc1 = fix.resp.items.at(1).proc; + proc1.num_calls() + .reset(1) + .on_num_meta(1) + .on_meta(1) + .on_row_batch_start(1) + .on_row(2) + .on_row_batch_finish(1) + .on_row_ok_packet(1) + .validate(); + BOOST_TEST(proc1.info() == "2nd"); +} + +BOOST_AUTO_TEST_CASE(prepare_statement_success) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::prepare_statement, 42u, {}}, + {pipeline_stage_kind::prepare_statement, 11u, {}}, + } + }; + fixture fix(stages); + + // Run the test. 1st statement has 2 meta, 2nd has 1 + algo_test() + .expect_write(mock_request) + .expect_read(prepare_stmt_response_builder().seqnum(42).id(7).num_columns(0).num_params(2).build()) + .expect_read(create_coldef_frame(43, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(44, meta_builder().name("def").build_coldef())) + .expect_read(prepare_stmt_response_builder().seqnum(11).id(9).num_columns(0).num_params(1).build()) + .expect_read(create_coldef_frame(12, meta_builder().name("aaa").build_coldef())) + .check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // Check the resulting statements + auto stmt0 = fix.resp.items.at(0).stmt; + BOOST_TEST(stmt0.id() == 7u); + BOOST_TEST(stmt0.num_params() == 2u); + + auto stmt1 = fix.resp.items.at(1).stmt; + BOOST_TEST(stmt1.id() == 9u); + BOOST_TEST(stmt1.num_params() == 1u); +} + +BOOST_AUTO_TEST_CASE(close_statement_success) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::close_statement, 3u, {}}, + {pipeline_stage_kind::close_statement, 8u, {}}, + } + }; + fixture fix(stages); + + // Run the test. Close statement doesn't have a response + algo_test().expect_write(mock_request).check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); +} + +BOOST_AUTO_TEST_CASE(reset_connection) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 3u, {}}, + } + }; + fixture fix(stages); + fix.st.current_charset = utf8mb4_charset; + + // Run the test + algo_test().expect_write(mock_request).expect_read(create_ok_frame(3, ok_builder().build())).check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // The current character set was reset + BOOST_TEST(fix.st.charset_ptr() == nullptr); +} + +BOOST_AUTO_TEST_CASE(set_character_set) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::set_character_set, 19u, utf8mb4_charset}, + } + }; + fixture fix(stages); + + // Run the test + algo_test().expect_write(mock_request).expect_read(create_ok_frame(19, ok_builder().build())).check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // The current character set was set + BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); +} + +BOOST_AUTO_TEST_CASE(ping) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::ping, 32u, {}}, + } + }; + fixture fix(stages); + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read(create_ok_frame(32, ok_builder().no_backslash_escapes(true).build())) + .check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // The OK packet was processed successfully + BOOST_TEST(fix.st.backslash_escapes == false); +} + +BOOST_AUTO_TEST_CASE(combination) +{ + // Setup. Typical connection setup pipeline, where we reset, set names, + // set the time_zone and prepare some statements + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + {pipeline_stage_kind::prepare_statement, 0u, {}}, + {pipeline_stage_kind::prepare_statement, 1u, {}}, + } + }; + fixture fix(stages); + fix.st.backslash_escapes = false; + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read(create_ok_frame(32, ok_builder().build())) + .expect_read(create_ok_frame(16, ok_builder().build())) + .expect_read(create_ok_frame(10, ok_builder().build())) + .expect_read(prepare_stmt_response_builder().seqnum(0).id(3).num_columns(1).num_params(1).build()) + .expect_read(create_coldef_frame(1, meta_builder().name("abc").build_coldef())) + .expect_read(create_coldef_frame(2, meta_builder().name("def").build_coldef())) + .expect_read(prepare_stmt_response_builder().seqnum(1).id(1).num_columns(0).num_params(0).build()) + .check(fix); + + // Setup was called correctly and all stages succeeded + fix.check_setup(stages); + fix.check_all_stages_succeeded(); + + // The pipeline had its intended effect + BOOST_TEST(fix.st.backslash_escapes == true); + BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); + BOOST_TEST(fix.resp.items.at(3).stmt.id() == 3u); + BOOST_TEST(fix.resp.items.at(4).stmt.id() == 1u); +} + +BOOST_AUTO_TEST_CASE(no_requests) +{ + // Setup + fixture fix({}, {}); + + // Run the test. We complete immediately + algo_test().check(fix); + + // Setup was called correctly + fix.check_setup({}); +} + +BOOST_AUTO_TEST_CASE(error_writing_request) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test. No response reading is attempted + algo_test().expect_write(mock_request, asio::error::eof).check(fix, asio::error::eof); + + // Setup was called correctly + fix.check_setup(stages); + + // All requests were marked as failed + fix.check_stage_error(0, {asio::error::eof, {}}); + fix.check_stage_error(1, {asio::error::eof, {}}); + fix.check_stage_error(2, {asio::error::eof, {}}); +} + +BOOST_AUTO_TEST_CASE(nonfatal_errors) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::prepare_statement, 32u, {}}, + {pipeline_stage_kind::prepare_statement, 16u, {}}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test. Steps 1 and 3 fail. + // The first error is the operation's result + algo_test() + .expect_write(mock_request) + .expect_read(err_builder() + .seqnum(32) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) + .expect_read(prepare_stmt_response_builder().seqnum(16).id(3).num_columns(0).num_params(0).build()) + .expect_read(err_builder() + .seqnum(10) + .code(common_server_errc::er_bad_field_error) + .message("other_msg") + .build_frame()) + .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); + + // Setup was called correctly + fix.check_setup(stages); + + // Stage errors + fix.check_stage_error(0, {common_server_errc::er_bad_db_error, create_server_diag("my_message")}); + fix.check_stage_error(1, {error_code(), {}}); + fix.check_stage_error(2, {common_server_errc::er_bad_field_error, create_server_diag("other_msg")}); + + // The operation that succeeded had its result set + BOOST_TEST(fix.resp.items.at(1).stmt.id() == 3u); +} + +BOOST_AUTO_TEST_CASE(nonfatal_errors_middle) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::prepare_statement, 32u, {}}, + {pipeline_stage_kind::prepare_statement, 16u, {}}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test. Steps 1 and 3 fail. + // The first error is the operation's result + algo_test() + .expect_write(mock_request) + .expect_read(prepare_stmt_response_builder().seqnum(32).id(3).num_columns(0).num_params(0).build()) + .expect_read(err_builder() + .seqnum(16) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) + .expect_read(create_ok_frame(10, ok_builder().no_backslash_escapes(true).build())) + .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); + + // Setup was called correctly + fix.check_setup(stages); + + // Stage errors + fix.check_stage_error(0, {}); + fix.check_stage_error(1, {common_server_errc::er_bad_db_error, create_server_diag("my_message")}); + fix.check_stage_error(2, {}); + + // We processed the OK packet correctly + BOOST_TEST(fix.st.backslash_escapes == false); +} + +BOOST_AUTO_TEST_CASE(fatal_error_first) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test. Reading the first response fails, and we don't further reading + algo_test() + .expect_write(mock_request) + .expect_read(asio::error::network_reset) + .check(fix, asio::error::network_reset); + + // Setup was called correctly + fix.check_setup(stages); + + // All subsequent requests were marked as failed + fix.check_stage_error(0, {asio::error::network_reset, {}}); + fix.check_stage_error(1, {asio::error::network_reset, {}}); + fix.check_stage_error(2, {asio::error::network_reset, {}}); +} + +BOOST_AUTO_TEST_CASE(fatal_error_middle) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read(create_ok_frame(32, ok_builder().build())) + .expect_read(asio::error::network_reset) + .check(fix, asio::error::network_reset); + + // Setup was called correctly + fix.check_setup(stages); + + // All subsequent requests were marked as failed + fix.check_stage_error(0, {}); + fix.check_stage_error(1, {asio::error::network_reset, {}}); + fix.check_stage_error(2, {asio::error::network_reset, {}}); +} + +// If there are fatal and non-fatal errors, the fatal one is the result of the operation +BOOST_AUTO_TEST_CASE(nonfatal_then_fatal_error) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read(err_builder() + .seqnum(32) + .code(common_server_errc::er_bad_db_error) + .message("my_message") + .build_frame()) + .expect_read(asio::error::already_connected) + .check(fix, asio::error::already_connected); + + // Setup was called correctly + fix.check_setup(stages); + + // Stage results + fix.check_stage_error(0, {common_server_errc::er_bad_db_error, create_server_diag("my_message")}); + fix.check_stage_error(1, {asio::error::already_connected, {}}); + fix.check_stage_error(2, {asio::error::already_connected, {}}); +} + +// Edge case: fatal error with non-empty diagnostics +BOOST_AUTO_TEST_CASE(fatal_error_with_diag) +{ + // Setup + const std::array stages{ + { + {pipeline_stage_kind::reset_connection, 32u, {}}, + {pipeline_stage_kind::set_character_set, 16u, utf8mb4_charset}, + {pipeline_stage_kind::execute, 10u, resultset_encoding::text}, + } + }; + fixture fix(stages); + + // Run the test + algo_test() + .expect_write(mock_request) + .expect_read( + err_builder().seqnum(32).code(common_server_errc::er_bad_db_error).message("bad db").build_frame() + ) + .expect_read(err_builder() + .seqnum(16) + .code(common_server_errc::er_aborting_connection) + .message("aborting connection") + .build_frame()) + .check(fix, common_server_errc::er_aborting_connection, create_server_diag("aborting connection")); + + // Setup was called correctly + fix.check_setup(stages); + + // Stage results + fix.check_stage_error(0, {common_server_errc::er_bad_db_error, create_server_diag("bad db")}); + fix.check_stage_error( + 1, + {common_server_errc::er_aborting_connection, create_server_diag("aborting connection")} + ); + fix.check_stage_error( + 2, + {common_server_errc::er_aborting_connection, create_server_diag("aborting connection")} + ); +} + +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 index ff39dc0a..71369383 100644 --- a/test/unit/test/sansio/set_character_set.cpp +++ b/test/unit/test/sansio/set_character_set.cpp @@ -17,12 +17,10 @@ #include #include -#include - +#include "test_common/create_basic.hpp" #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" #include "test_unit/create_query_frame.hpp" @@ -32,63 +30,100 @@ using namespace boost::mysql; BOOST_AUTO_TEST_SUITE(test_set_character_set) -struct fixture : algo_fixture_base +// +// compose_set_names +// +BOOST_AUTO_TEST_CASE(compose_set_names_success) { - detail::set_character_set_algo algo; + BOOST_TEST(detail::compose_set_names(utf8mb4_charset).value() == "SET NAMES 'utf8mb4'"); + BOOST_TEST(detail::compose_set_names(ascii_charset).value() == "SET NAMES 'ascii'"); +} - fixture(const character_set& charset = utf8mb4_charset) : algo(st, {&diag, charset}) {} +BOOST_AUTO_TEST_CASE(compose_set_names_needs_escaping) +{ + // We don't create vulnerabilities when creating SET NAMES statements + character_set mock_charset{"ab'cd\"e", utf8mb4_charset.next_char}; + BOOST_TEST(detail::compose_set_names(mock_charset).value() == "SET NAMES 'ab\\'cd\\\"e'"); +} + +BOOST_AUTO_TEST_CASE(compose_set_names_error) +{ + struct + { + string_view name; + string_view charset_name; + } test_cases[] = { + {"utf8", "test-\xc3\xb1" }, // anything non-ascii is rejected + {"non_utf8", "test-\xb1-abc" }, + {"null_character", makesv("with\0null")}, // NULL characters not allowed + }; + + for (const auto& tc : test_cases) + { + BOOST_TEST_CONTEXT(tc.charset_name) + { + character_set charset{tc.charset_name, utf8mb4_charset.next_char}; + BOOST_TEST(detail::compose_set_names(charset).error() == client_errc::invalid_encoding); + } + } +} + +// +// read_set_character_set_response_algo +// +struct read_response_fixture : algo_fixture_base +{ + detail::read_set_character_set_response_algo algo; + + // Clearing diagnostics is not this algorithm's responsibility + read_response_fixture(character_set charset = utf8mb4_charset) + : algo_fixture_base(diagnostics()), algo({&diag, charset, 29}) + { + } }; -BOOST_AUTO_TEST_CASE(success) +BOOST_AUTO_TEST_CASE(read_response_success) { // Setup - fixture fix; + read_response_fixture fix; // Run the algo - algo_test() - .expect_write(create_query_frame(0, "SET NAMES 'utf8mb4'")) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check(fix); + algo_test().expect_read(create_ok_frame(29, ok_builder().build())).check(fix); // The charset was updated BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); } -BOOST_AUTO_TEST_CASE(success_previous_charset) +BOOST_AUTO_TEST_CASE(read_response_success_previous_charset) { // Setup - fixture fix; + read_response_fixture fix; fix.st.current_charset = ascii_charset; // Run the algo - algo_test() - .expect_write(create_query_frame(0, "SET NAMES 'utf8mb4'")) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check(fix); + algo_test().expect_read(create_ok_frame(29, ok_builder().build())).check(fix); // The charset was updated BOOST_TEST(fix.st.charset_ptr()->name == "utf8mb4"); } -BOOST_AUTO_TEST_CASE(error_network) +BOOST_AUTO_TEST_CASE(read_response_error_network) { // This covers errors in read and write algo_test() - .expect_write(create_query_frame(0, "SET NAMES 'utf8mb4'")) - .expect_read(create_ok_frame(1, ok_builder().build())) - .check_network_errors(); + .expect_read(create_ok_frame(29, ok_builder().build())) + .check_network_errors(); } -BOOST_AUTO_TEST_CASE(error_response) +BOOST_AUTO_TEST_CASE(read_response_error_packet) { // Setup - fixture fix; + read_response_fixture fix; // Run the algo algo_test() - .expect_write(create_query_frame(0, "SET NAMES 'utf8mb4'")) .expect_read(err_builder() - .seqnum(1) + .seqnum(29) .code(common_server_errc::er_unknown_character_set) .message("Unknown charset") .build_frame()) @@ -98,14 +133,36 @@ BOOST_AUTO_TEST_CASE(error_response) BOOST_TEST(fix.st.charset_ptr() == nullptr); } -// Character set function that always returns 1 -static std::size_t stub_next_char(boost::span) noexcept { return 1u; } +// +// set_character_set_algo +// +struct set_charset_fixture : algo_fixture_base +{ + detail::set_character_set_algo algo; -// Ensure we don't create vulnerabilities when composing SET NAMES -BOOST_AUTO_TEST_CASE(charset_name_needs_escaping) + set_charset_fixture(character_set charset = utf8mb4_charset) : algo({&diag, charset}) {} +}; + +BOOST_AUTO_TEST_CASE(set_charset_success) { // Setup - fixture fix({"lat'in\\", stub_next_char}); + set_charset_fixture fix; + + // Run the algo + algo_test() + .expect_write(create_query_frame(0, "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 == "utf8mb4"); +} + +// Ensure we don't create vulnerabilities when composing SET NAMES +BOOST_AUTO_TEST_CASE(set_charset_name_needs_escaping) +{ + // Setup + set_charset_fixture fix({"lat'in\\", utf8mb4_charset.next_char}); // Run the algo algo_test() @@ -114,16 +171,26 @@ BOOST_AUTO_TEST_CASE(charset_name_needs_escaping) .check(fix); } -BOOST_AUTO_TEST_CASE(error_nonascii_charset_name) +BOOST_AUTO_TEST_CASE(set_charset_error_composing_request) { // Setup - fixture fix({"lat\xc3\xadn", stub_next_char}); + set_charset_fixture fix({"lat\xc3\xadn", utf8mb4_charset.next_char}); + fix.st.current_charset = ascii_charset; // Run the algo algo_test().check(fix, client_errc::invalid_encoding); // The current character set was not updated - BOOST_TEST(fix.st.charset_ptr() == nullptr); + BOOST_TEST(fix.st.charset_ptr()->name == "ascii"); +} + +BOOST_AUTO_TEST_CASE(set_charset_error_network) +{ + // This covers errors in reading the response + algo_test() + .expect_write(create_query_frame(0, "SET NAMES 'utf8mb4'")) + .expect_read(create_ok_frame(1, ok_builder().build())) + .check_network_errors(); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/start_execution.cpp b/test/unit/test/sansio/start_execution.cpp index 8a3d3edb..0defc87c 100644 --- a/test/unit/test/sansio/start_execution.cpp +++ b/test/unit/test/sansio/start_execution.cpp @@ -42,7 +42,7 @@ struct fixture : algo_fixture_base mock_execution_processor proc; detail::start_execution_algo algo; - fixture(any_execution_request req) : algo(st, {&diag, req, &proc}) {} + fixture(any_execution_request req) : algo({&diag, req, &proc}) {} }; BOOST_AUTO_TEST_CASE(text_query) diff --git a/test/unit/test/sansio/top_level_algo.cpp b/test/unit/test/sansio/top_level_algo.cpp index 27a8cbd0..d15779c8 100644 --- a/test/unit/test/sansio/top_level_algo.cpp +++ b/test/unit/test/sansio/top_level_algo.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include @@ -53,26 +52,24 @@ const u8vec msg2(50, 0x04); BOOST_AUTO_TEST_CASE(read_cached) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return read(seqnum); + BOOST_ASIO_CORO_YIELD return st.read(seqnum); BOOST_TEST(ec == error_code()); BOOST_TEST(seqnum == 1u); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg1); - BOOST_ASIO_CORO_YIELD return read(seqnum); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st.reader.message(), msg1); + BOOST_ASIO_CORO_YIELD return st.read(seqnum); BOOST_TEST(ec == error_code()); BOOST_TEST(seqnum == 2u); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg2); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st.reader.message(), msg2); } return next_action(); } @@ -83,7 +80,7 @@ BOOST_AUTO_TEST_CASE(read_cached) // Initial run yields a read request. We don't have cached data, so run_op returns it auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data()); BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size()); BOOST_TEST(!act.read_args().use_ssl); @@ -99,22 +96,20 @@ BOOST_AUTO_TEST_CASE(read_cached) BOOST_AUTO_TEST_CASE(read_short_and_buffer_resizing) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return read(seqnum); + BOOST_ASIO_CORO_YIELD return st.read(seqnum); BOOST_TEST(ec == error_code()); BOOST_TEST(seqnum == 1u); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg2); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st.reader.message(), msg2); } return next_action(); } @@ -125,7 +120,7 @@ BOOST_AUTO_TEST_CASE(read_short_and_buffer_resizing) // Initial run yields a read request and resizes the buffer aprorpiately auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data()); BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size()); BOOST_TEST(!act.read_args().use_ssl); @@ -136,12 +131,12 @@ BOOST_AUTO_TEST_CASE(read_short_and_buffer_resizing) act = algo.resume(error_code(), 4); // The read request wasn't completely satisified, so more bytes are asked for - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); // Read part of the body transfer(act.read_args().buffer, span(bytes.data() + 4, 10)); act = algo.resume(error_code(), 10); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); // Complete transfer(act.read_args().buffer, span(bytes.data() + 14, bytes.size() - 14)); @@ -151,19 +146,17 @@ BOOST_AUTO_TEST_CASE(read_short_and_buffer_resizing) BOOST_AUTO_TEST_CASE(read_parsing_error) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{42u}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return read(seqnum); + BOOST_ASIO_CORO_YIELD return st.read(seqnum); BOOST_TEST(ec == error_code(client_errc::sequence_number_mismatch)); } return next_action(); @@ -175,7 +168,7 @@ BOOST_AUTO_TEST_CASE(read_parsing_error) // Initial run yields a read request. We don't have cached data, so run_op returns it auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); // Acknowledge the read request. This causes a seqnum mismatch that is transmitted to the op auto bytes = create_frame(0, msg1); @@ -188,19 +181,17 @@ BOOST_AUTO_TEST_CASE(read_parsing_error) BOOST_AUTO_TEST_CASE(read_io_error) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return read(seqnum); + BOOST_ASIO_CORO_YIELD return st.read(seqnum); BOOST_TEST(ec == error_code(client_errc::wrong_num_params)); } return next_action(); @@ -212,7 +203,7 @@ BOOST_AUTO_TEST_CASE(read_io_error) // Initial run yields a read request. We don't have cached data, so run_op returns it auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); // Read request fails with an error act = algo.resume(client_errc::wrong_num_params, 0); @@ -223,16 +214,14 @@ BOOST_AUTO_TEST_CASE(read_io_error) BOOST_AUTO_TEST_CASE(read_ssl_active) { - struct mock_algo : sansio_algorithm + struct mock_algo { std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_TEST(ec == error_code()); - return read(seqnum); + return st.read(seqnum); } }; @@ -242,7 +231,7 @@ BOOST_AUTO_TEST_CASE(read_ssl_active) // Yielding a read with ssl active sets the use_ssl flag auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::read); + BOOST_TEST(act.type() == next_action_type::read); BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data()); BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size()); BOOST_TEST(act.read_args().use_ssl); @@ -250,19 +239,17 @@ BOOST_AUTO_TEST_CASE(read_ssl_active) BOOST_AUTO_TEST_CASE(write_short) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return write(mock_message{msg1}, seqnum); + BOOST_ASIO_CORO_YIELD return st.write(mock_message{msg1}, seqnum); BOOST_TEST(ec == error_code()); BOOST_TEST(seqnum == 1u); } @@ -275,13 +262,13 @@ BOOST_AUTO_TEST_CASE(write_short) // Initial run yields a write request auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::write); + BOOST_TEST(act.type() == next_action_type::write); BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, create_frame(0, msg1)); BOOST_TEST(!act.write_args().use_ssl); // Acknowledge part of the write. This will ask for more bytes to be written act = algo.resume(error_code(), 4); - BOOST_TEST(act.type() == next_action::type_t::write); + BOOST_TEST(act.type() == next_action_type::write); BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, msg1); // Complete @@ -291,19 +278,17 @@ BOOST_AUTO_TEST_CASE(write_short) BOOST_AUTO_TEST_CASE(write_io_error) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { BOOST_TEST(ec == error_code()); - BOOST_ASIO_CORO_YIELD return write(mock_message{msg1}, seqnum); + BOOST_ASIO_CORO_YIELD return st.write(mock_message{msg1}, seqnum); BOOST_TEST(ec == error_code(client_errc::wrong_num_params)); } return next_action(); @@ -315,7 +300,7 @@ BOOST_AUTO_TEST_CASE(write_io_error) // Initial run yields a write request. Fail it auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::write); + BOOST_TEST(act.type() == next_action_type::write); act = algo.resume(client_errc::wrong_num_params, 0); // Done @@ -324,16 +309,14 @@ BOOST_AUTO_TEST_CASE(write_io_error) BOOST_AUTO_TEST_CASE(write_ssl_active) { - struct mock_algo : sansio_algorithm + struct mock_algo { std::uint8_t seqnum{}; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data& st, error_code ec) { BOOST_TEST(ec == error_code()); - return write(mock_message{msg1}, seqnum); + return st.write(mock_message{msg1}, seqnum); } }; @@ -343,20 +326,18 @@ BOOST_AUTO_TEST_CASE(write_ssl_active) // Yielding a write request when ssl_active() returns an action with the flag set auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::write); + BOOST_TEST(act.type() == next_action_type::write); BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, create_frame(0, msg1)); BOOST_TEST(act.write_args().use_ssl); } BOOST_AUTO_TEST_CASE(ssl_handshake) { - struct mock_algo : sansio_algorithm + struct mock_algo { boost::asio::coroutine coro; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data&, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { @@ -373,7 +354,7 @@ BOOST_AUTO_TEST_CASE(ssl_handshake) // Initial run yields a SSL handshake request. These are always returned auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::ssl_handshake); + BOOST_TEST(act.type() == next_action_type::ssl_handshake); // Fail the op act = algo.resume(client_errc::wrong_num_params, 0); @@ -384,13 +365,11 @@ BOOST_AUTO_TEST_CASE(ssl_handshake) BOOST_AUTO_TEST_CASE(ssl_shutdown) { - struct mock_algo : sansio_algorithm + struct mock_algo { coroutine coro; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data&, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { @@ -407,7 +386,7 @@ BOOST_AUTO_TEST_CASE(ssl_shutdown) // Initial run yields a SSL handshake request. These are always returned auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::ssl_shutdown); + BOOST_TEST(act.type() == next_action_type::ssl_shutdown); // Fail the op act = algo.resume(client_errc::wrong_num_params, 0); @@ -418,13 +397,11 @@ BOOST_AUTO_TEST_CASE(ssl_shutdown) BOOST_AUTO_TEST_CASE(connect) { - struct mock_algo : sansio_algorithm + struct mock_algo { boost::asio::coroutine coro; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data&, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { @@ -441,7 +418,7 @@ BOOST_AUTO_TEST_CASE(connect) // Initial run yields a connect request. These are always returned auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::connect); + BOOST_TEST(act.type() == next_action_type::connect); // Fail the op act = algo.resume(client_errc::wrong_num_params, 0); @@ -452,13 +429,11 @@ BOOST_AUTO_TEST_CASE(connect) BOOST_AUTO_TEST_CASE(close) { - struct mock_algo : sansio_algorithm + struct mock_algo { boost::asio::coroutine coro; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data&, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { @@ -475,7 +450,7 @@ BOOST_AUTO_TEST_CASE(close) // Initial run yields a close request. These are always returned auto act = algo.resume(error_code(), 0); - BOOST_TEST(act.type() == next_action::type_t::close); + BOOST_TEST(act.type() == next_action_type::close); // Fail the op act = algo.resume(client_errc::wrong_num_params, 0); @@ -486,13 +461,11 @@ BOOST_AUTO_TEST_CASE(close) BOOST_AUTO_TEST_CASE(immediate_completion) { - struct mock_algo : sansio_algorithm + struct mock_algo { boost::asio::coroutine coro; - mock_algo(connection_state_data& st) : sansio_algorithm(st) {} - - next_action resume(error_code ec) + next_action resume(connection_state_data&, error_code ec) { BOOST_ASIO_CORO_REENTER(coro) { diff --git a/test/unit/test/spotchecks/misc.cpp b/test/unit/test/spotchecks/misc.cpp index 32e25b09..2dd1e71b 100644 --- a/test/unit/test/spotchecks/misc.cpp +++ b/test/unit/test/spotchecks/misc.cpp @@ -5,15 +5,23 @@ // 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 #include #include +#include +#include +#include +#include + #include #include #include @@ -24,6 +32,7 @@ #include "test_common/assert_buffer_equals.hpp" #include "test_common/buffer_concat.hpp" #include "test_common/netfun_maker.hpp" +#include "test_common/printing.hpp" #include "test_unit/create_coldef_frame.hpp" #include "test_unit/create_execution_processor.hpp" #include "test_unit/create_frame.hpp" @@ -421,4 +430,64 @@ BOOST_AUTO_TEST_CASE(stmt_tuple_ref) BOOST_CHECK_NO_THROW(conn.execute(stmt.bind(std::ref(s), std::ref(b)), r)); } +// Helper to create any_connection objects using test_stream +struct test_any_connection_fixture +{ + any_connection conn; + + test_any_connection_fixture() + : conn(detail::access::construct( + default_initial_read_buffer_size, + std::unique_ptr( + new detail::engine_impl>() + ) + )) + { + } + + test_stream& stream() + { + return detail::stream_from_engine(detail::access::get_impl(conn).get_engine()); + } +}; + +auto pipeline_fn = netfun_maker_mem< + void, + any_connection, + const pipeline_request&, + pipeline_request::response_type&>::async_errinfo(&any_connection::async_run_pipeline); + +// empty pipelines complete immediately, posting adequately +BOOST_FIXTURE_TEST_CASE(empty_dynamic_pipeline, test_any_connection_fixture) +{ + // Setup + pipeline_request req; + pipeline_request::response_type res; + + // Run it. It should complete immediately, posting to the correct executor (verified by the testing + // infrastructure) + pipeline_fn(conn, req, res).validate_no_error(); + BOOST_TEST(res.size() == 0u); +} + +// fatal errors in pipelines behave correctly +BOOST_FIXTURE_TEST_CASE(pipeline_fatal_error, test_any_connection_fixture) +{ + // Setup + pipeline_request req; + pipeline_request::response_type res; + req.add(execute_stage("SELECT 1")).add(execute_stage("SELECT 2")); + + // The first read will fail + stream().set_fail_count(fail_count(1, boost::asio::error::network_reset)); + + // Run it + pipeline_fn(conn, req, res).validate_error_exact(boost::asio::error::network_reset); + + // Validate the results + BOOST_TEST(res.size() == 2u); + BOOST_TEST(res[0].error() == (errcode_with_diagnostics{boost::asio::error::network_reset, {}})); + BOOST_TEST(res[1].error() == (errcode_with_diagnostics{boost::asio::error::network_reset, {}})); +} + BOOST_AUTO_TEST_SUITE_END()