diff --git a/.github/workflows/build-code.yml b/.github/workflows/build-code.yml index 606e0471..32e22a09 100644 --- a/.github/workflows/build-code.yml +++ b/.github/workflows/build-code.yml @@ -19,9 +19,11 @@ jobs: OPENSSL_ROOT: /usr/local/opt/openssl steps: - uses: actions/checkout@v3 + - run: unlink /usr/local/bin/python + - run: ln -s /usr/local/bin/python3 /usr/local/bin/python - run: bash tools/setup_db_osx.sh - run: | - python3 tools/ci.py \ + python tools/ci.py \ --build-kind=b2 \ --source-dir=$(pwd) \ --toolset=clang \ diff --git a/.gitignore b/.gitignore index e07705da..d232946c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ build/ .project .settings/ .pydevproject -private/ +/private/ build-*/ CMakeSettings.json out/ diff --git a/doc/images/protocol_multi_resultset.svg b/doc/images/protocol_multi_resultset.svg new file mode 100644 index 00000000..9c44820e --- /dev/null +++ b/doc/images/protocol_multi_resultset.svg @@ -0,0 +1,4 @@ + + + +
Client
Client
Server
Server
query request / statement execution request
query request / statement execution request
execute successful, metadata follows
execute successful, metadata follows
metadata packets, 1 per column
metadata packets, 1 per column
row packets, 1 per row
row packets, 1 per row
OK packet, another resultset follows
OK packet, another resultset follows
start_query()
start_statement_execution()
start_query()...
read_some_rows()
read_some_rows()
query()
execute_statement()
query()...
Operation retrieving multiple
resultsets
Operation retrieving multiple...
metadata follows
metadata follows
metadata packets, 1 per column
metadata packets, 1 per column
row packets, 1 per row
row packets, 1 per row
OK packet, no more resultsets follow
OK packet, no more resultsets follow
read_resultset_head()
read_resultset_head...
read_some_rows()
read_some_rows()
First resultset
First resultset
Second resultset
Second resultset
Text is not SVG - cannot display
\ No newline at end of file diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 14700cf6..b72729a3 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -22,6 +22,7 @@ [template nochunk[] [block '''''']] [template mdash[] '''— '''] +[template link_to_file[path][^''''''[path]'''''']] [template include_file[path][^<''''''[path]''''''>]] [template indexterm1[term1] ''''''[term1]''''''] [template indexterm2[term1 term2] ''''''[term1]''''''[term2]''''''] @@ -91,18 +92,19 @@ [include 03_overview.qbk] [include 04_queries.qbk] [include 05_prepared_statements.qbk] -[include 06_multi_function.qbk] -[include 07_fields.qbk] -[include 08_metadata.qbk] -[include 09_async.qbk] -[include 10_ssl.qbk] -[include 11_other_streams.qbk] -[include 12_error_handling.qbk] -[include 13_connparams.qbk] -[include 14_reconnecting.qbk] -[include 15_charsets.qbk] -[include 16_examples.qbk] -[include 17_tests.qbk] +[include 06_multi_resultset.qbk] +[include 07_multi_function.qbk] +[include 08_fields.qbk] +[include 09_metadata.qbk] +[include 10_async.qbk] +[include 11_ssl.qbk] +[include 12_other_streams.qbk] +[include 13_error_handling.qbk] +[include 14_connparams.qbk] +[include 15_reconnecting.qbk] +[include 16_charsets.qbk] +[include 17_examples.qbk] +[include 18_tests.qbk] [section:ref Reference] diff --git a/doc/qbk/04_queries.qbk b/doc/qbk/04_queries.qbk index 6bf8098e..dac3e0fc 100644 --- a/doc/qbk/04_queries.qbk +++ b/doc/qbk/04_queries.qbk @@ -22,20 +22,29 @@ commit them using `COMMIT` and rolling them back using `ROLLBACK`. [heading Limitations] -* You can only execute a single query at a time. Passing in several queries separated by semicolons - will produce an error. -* At the moment, there is no equivalent to - [@https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-escape-string.html `mysql_real_escape_string`] - to sanitize user provided input. This limits text queries to queries without parameters. - Doing composition by hand can lead to SQL injection vulnerabilities. - Please use [link mysql.prepared_statements prepared statements] - instead, which perform composition server-side in a safe way. +At the moment, there is no equivalent to +[@https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-escape-string.html `mysql_real_escape_string`] +to sanitize user provided input. This limits text queries to queries without parameters. +Doing composition by hand can lead to SQL injection vulnerabilities. +Please use [link mysql.prepared_statements prepared statements] +instead, which perform composition server-side in a safe way. [warning [*SQL injection warning]: if you compose queries by concatenating strings without sanitization, your code is vulnerable to SQL injection attacks. Use prepared statements when possible! ] +[heading Running multiple queries at once] + +You can run several semicolon-separated queries in a single call by enabling +the [refmem handshake_params multi_queries] option. You can find an example +[link mysql.multi_resultset.multi_queries here]. + +[warning + [*SQL injection warning]: do not use this feature if any of your queries contains + untrusted input. You can create SQL injection vulnerabilities if you do. +] + [heading Use cases] You should generally prefer prepared statements over text queries. Text queries can be useful for simple, diff --git a/doc/qbk/06_multi_resultset.qbk b/doc/qbk/06_multi_resultset.qbk new file mode 100644 index 00000000..5653f9c4 --- /dev/null +++ b/doc/qbk/06_multi_resultset.qbk @@ -0,0 +1,104 @@ +[/ + Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) + + Distributed under the Boost Software License, Version 1.0. (See accompanying + file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +] + +[section:multi_resultset Multi-resultset: stored procedures and multi-queries] + +[heading Using stored procedures] + +[mysqllink create-procedure.html Stored procedures] can be called using the `CALL` SQL statement. +You can use `CALL` statements from both text queries and prepared statements, in a similar way +to other SQL statements. Contrary to other statements, `CALL` may generate more than one resultset. + +For example, given a stored procedure like this: + +[multi_resultset_procedure] + +You can call it using a prepared statement, with the usual syntax: + +[multi_resultset_call] + +MySQL responds here with two resultsets: + +* A first resultset containing the employee matched by the `SELECT` query in the procedure. +* A second, empty resultset containing information about the `CALL` statement. + +The [reflink results] class can store any number of resultsets. +It can be understood as a random-access collection of resultsets. To obtain the first resultset, we can write: + +[multi_resultset_first_resultset] + +[heading Determining the number of resultsets] + +To know the number of resultsets to expect from a `CALL` statement, use these rules: + +* For every statement that retrieves data (e.g. a `SELECT` statement), a resultset is sent. + `SELECT ... INTO ` statements don't cause a resultset to be sent. +* Statements that don't retrieve columns (e.g. `UPDATE`, `DELETE`) don't cause a resultset to be sent. +* An empty resultset containing information about the `CALL` statement execution is always sent last. + +Some examples: + +[!teletype] +``` + -- Calling proc1 produces only 1 resultset because it only contains statements that + -- don't retrieve data + CREATE PROCEDURE proc1(IN pin_order_id INT, IN pin_quantity INT) + BEGIN + START TRANSACTION; + UPDATE orders SET quantity = pin_quantity WHERE id = pin_order_id; + INSERT INTO audit_log (msg) VALUES ("Updated order..."); + COMMIT; + END +``` + +[!teletype] +``` + -- Calling proc2 produces 3 resultsets: one for the orders SELECT, one for the + -- line_items SELECT, and one for the CALL statement + CREATE PROCEDURE proc2(IN pin_order_id INT) + BEGIN + START TRANSACTION READ ONLY; + SELECT * FROM orders WHERE id = pin_order_id; + SELECT * FROM line_items WHERE order_id = pin_order_id; + COMMIT; + END +``` + +[heading Output parameters] + +You can get the value of `OUT` and `INOUT` parameters in stored procedures by using +prepared statement placeholders for them. When doing this, you will receive another resultset +with a single row containing all output parameter values. This resultset is located after +all resultsets generated by `SELECT`s, and before the final, empty resultset. +To simplify things, you can use [refmem results out_params] to retrieve them: + +[multi_resultset_out_params] + +[warning + Due to a bug in MySQL, some `OUT` parameters are sent with wrong types. + Concretely, string parameters are always sent as blobs, so you will have to + use [refmem field_view as_blob] instead of [refmem field_view as_string]. +] + +[heading:multi_queries Semicolon-separated queries] + +It is possible to run several semicolon-separated text queries in a single [refmem connection query] call. +For security, this capability is disabled by default. Enabling it requires setting [refmem handshake_params multi_queries] +before connecting: + +[multi_resultset_multi_queries] + +You can access the resultsets produced by individual queries using [refmem results at] and [reflink2 results.operator_lb__rb_ results::operator[]]. +These operations yield a [reflink resultset_view], which is a lighteight object pointing into memory owned by the `results` +object. You can take ownserhip of a `resultset_view` using the [reflink resultset] class. For example: + +[multi_resultset_results_as_collection] + +Note that statements like `DELIMITER` [*do not work] using this feature. This is because +`DELIMITER` is a pseudo-command for the `mysql` command line tool, not actual SQL. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/06_multi_function.qbk b/doc/qbk/07_multi_function.qbk similarity index 72% rename from doc/qbk/06_multi_function.qbk rename to doc/qbk/07_multi_function.qbk index 27b37245..fed1a10b 100644 --- a/doc/qbk/06_multi_function.qbk +++ b/doc/qbk/07_multi_function.qbk @@ -107,8 +107,49 @@ of useful information in this object, like the column name, its type, whether it You can access OK packet data using functions like [refmem execution_state last_insert_id] and [refmem execution_state affected_rows]. As this information is contained in the OK packet, -[*these functions have `st.complete() == true` as precondition]. +[*it can only be accessed once [refmem execution_state complete] returns `true`]. +[heading Using multi-function operations with stored procedures and multi-queries] + +When using operations that return more than one resultset (e.g. when calling stored procedures), +the protocol is slightly more complex: + +[$mysql/images/protocol_multi_resultset.svg [align center]] + +The message exchange is as follows: + +* A single execution request is sent to the server. +* The server sends a first resultset, as in the single resultset case. + The OK packet contains a flag indicating that another resultset follows. +* Another resultset is sent, with the same structure as the previous one. The process is + repeated until an OK packet indicates that no more resultsets follow. + +This can be translated into the following code: + +[multi_function_stored_procedure] + +Note that we're using [refmem execution_state should_read_rows] instead of [refmem execution_state complete] +as our loop termination condition. `complete()` returns true when all the resultsets have been read, +while `should_read_rows()` will return false once an individual result has been fully read. + +[heading Multi-resultset in the general case] + +`execution_state` can be seen as a state machine with four states. Each state +describes which reading function should be invoked next: + +* [refmem execution_state should_start_op]: the initial state, after you default-construct an `execution_state`. + You should call functions like `start_query` or `start_statement_execution` to start the operation. +* [refmem execution_state should_read_rows]: the next operation should be [refmem connection read_some_rows], + to read the generated rows. +* [refmem execution_state should_read_head]: the next operation should be [refmem connection read_resultset_head], + to read the next resultset metadata. Only operations that generate multiple resultsets go into this state. +* [refmem execution_state complete]: no more operations are required. + +For multi-function operations, you may also access OK packet data ever time a resultset has completely +been read, i.e. when [refmem execution_state should_read_head] returns `true`. + +[link mysql.examples.source_script This example] shows this general case. It uses +multi-queries to run an arbitrary SQL script file. [heading:read_some_rows More on read_some_rows] diff --git a/doc/qbk/07_fields.qbk b/doc/qbk/08_fields.qbk similarity index 100% rename from doc/qbk/07_fields.qbk rename to doc/qbk/08_fields.qbk diff --git a/doc/qbk/08_metadata.qbk b/doc/qbk/09_metadata.qbk similarity index 100% rename from doc/qbk/08_metadata.qbk rename to doc/qbk/09_metadata.qbk diff --git a/doc/qbk/09_async.qbk b/doc/qbk/10_async.qbk similarity index 99% rename from doc/qbk/09_async.qbk rename to doc/qbk/10_async.qbk index 27aa8214..6ad10802 100644 --- a/doc/qbk/09_async.qbk +++ b/doc/qbk/10_async.qbk @@ -8,7 +8,7 @@ [section:async Going async] [nochunk] -Following __Asio__'s convetion, all network operations have +Following __Asio__'s convention, all network operations have asynchronous versions with the same name prefixed by `async_`. The last parameter to async operations is a __CompletionToken__, which dictates how the asynchronous operation will be managed diff --git a/doc/qbk/10_ssl.qbk b/doc/qbk/11_ssl.qbk similarity index 100% rename from doc/qbk/10_ssl.qbk rename to doc/qbk/11_ssl.qbk diff --git a/doc/qbk/11_other_streams.qbk b/doc/qbk/12_other_streams.qbk similarity index 100% rename from doc/qbk/11_other_streams.qbk rename to doc/qbk/12_other_streams.qbk diff --git a/doc/qbk/12_error_handling.qbk b/doc/qbk/13_error_handling.qbk similarity index 100% rename from doc/qbk/12_error_handling.qbk rename to doc/qbk/13_error_handling.qbk diff --git a/doc/qbk/13_connparams.qbk b/doc/qbk/14_connparams.qbk similarity index 94% rename from doc/qbk/13_connparams.qbk rename to doc/qbk/14_connparams.qbk index 99215e3f..031cc6b8 100644 --- a/doc/qbk/13_connparams.qbk +++ b/doc/qbk/14_connparams.qbk @@ -20,11 +20,11 @@ password is provided to __Self__ in plain text, but it is not sent like that to the server (see below for more info). If your password is empty, just provide an empty string. -MySQL implements several ways of authentication with the server, +MySQL implements several methods of authentication with the server, in what is called [mysqllink pluggable-authentication.html -pluggable authentication]. The decission of which authentication -plugin to use is made in a per-user basis. This information -is stored in the `mysql.user` table. In addition to this, +pluggable authentication]. The authentication +plugin used is chosen on a per-user basis. This information +is stored in the `mysql.user` table. Additionally, servers define a default authentication plugin (see [mysqllink server-system-variables.html#sysvar_authentication_policy `authentication_policy`] and [mysqllink server-system-variables.html#sysvar_default_authentication_plugin diff --git a/doc/qbk/14_reconnecting.qbk b/doc/qbk/15_reconnecting.qbk similarity index 100% rename from doc/qbk/14_reconnecting.qbk rename to doc/qbk/15_reconnecting.qbk diff --git a/doc/qbk/15_charsets.qbk b/doc/qbk/16_charsets.qbk similarity index 100% rename from doc/qbk/15_charsets.qbk rename to doc/qbk/16_charsets.qbk diff --git a/doc/qbk/16_examples.qbk b/doc/qbk/17_examples.qbk similarity index 83% rename from doc/qbk/16_examples.qbk rename to doc/qbk/17_examples.qbk index c3868f7d..a154f3d2 100644 --- a/doc/qbk/16_examples.qbk +++ b/doc/qbk/17_examples.qbk @@ -22,8 +22,10 @@ Here is a list of available examples: # [link mysql.examples.async_coroutines Async functions using stackful coroutines] # [link mysql.examples.async_coroutinescpp20 Async functions using C++20 coroutines] # [link mysql.examples.default_completion_tokens Async functions using default completion tokens] -# [link mysql.examples.timeouts Settings timeouts] +# [link mysql.examples.timeouts Setting timeouts] # [link mysql.examples.ssl Setting SSL options] +# [link mysql.examples.stored_procedures Using stored procedures] +# [link mysql.examples.source_script Using multi-queries to source a .sql file] [section:setup Setup] @@ -210,4 +212,41 @@ for you. [endsect] + +[section:stored_procedures Using stored procedures] + +This example demonstrates how to use stored procedures to +implement a minimal order management system for an online store. + +The example employs synchronous functions with +exceptions as error handling. __see_error_handling__ + +This examples requires you to run [link_to_file example/db_setup_stored_procedures.sql]. +You can find table and procedure definitions in this file. + +[import ../../example/stored_procedures.cpp] + +[example_stored_procedures] + +[endsect] + + +[section:source_script Using multi-queries to source a .sql file] + +This example demonstrates how to source a .sql script using the +[link mysql.multi_resultset.multi_queries multi-queries feature]. + +Note that commands like `DELIMITER` won't work, since these are handled +by the `mysql` command line tool, rather than the server. + +The example employs synchronous functions with +exceptions as error handling. __see_error_handling__ + +__assume_setup__ + +[import ../../example/source_script.cpp] +[example_source_script] + +[endsect] + [endsect] [/ examples] \ No newline at end of file diff --git a/doc/qbk/17_tests.qbk b/doc/qbk/18_tests.qbk similarity index 100% rename from doc/qbk/17_tests.qbk rename to doc/qbk/18_tests.qbk diff --git a/doc/qbk/helpers/quickref.xml b/doc/qbk/helpers/quickref.xml index 06d93046..341ce0bd 100644 --- a/doc/qbk/helpers/quickref.xml +++ b/doc/qbk/helpers/quickref.xml @@ -28,6 +28,8 @@ handshake_params metadata results + resultset_view + resultset row row_view rows diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index aed31b15..e164f916 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -9,54 +9,6 @@ # when we're the main project and BOOST_MYSQL_INTEGRATION_TESTS is on find_package(Boost ${BOOST_MYSQL_VERSION} REQUIRED COMPONENTS context) -# Compile the example -function (build_example EXECUTABLE_NAME CPPFILE) - add_executable( - ${EXECUTABLE_NAME} - ${CPPFILE} - ) - target_link_libraries( - ${EXECUTABLE_NAME} - PRIVATE - Boost::disable_autolinking - Boost::mysql - Boost::context - boost_mysql_asio - ) - common_target_settings(${EXECUTABLE_NAME}) -endfunction() - -# Run it as a test -function (test_example EXECUTABLE_NAME CONNECTION_ARG) - add_test( - NAME ${EXECUTABLE_NAME} - COMMAND ${EXECUTABLE_NAME} example_user example_password ${CONNECTION_ARG} - ) -endfunction() - -# Run it as a test using Valgrind -function (memcheck_example EXECUTABLE_NAME CONNECTION_ARG) - set(TEST_NAME "${EXECUTABLE_NAME}_memcheck") - add_memcheck_test( - NAME ${TEST_NAME} - TARGET ${EXECUTABLE_NAME} - ARGUMENTS example_user example_password ${CONNECTION_ARG} - ) -endfunction() - -# Build and run it as either of them -function (add_example EXAMPLE_NAME DO_MEMCHECK CONNECTION_ARG) - set(EXECUTABLE_NAME "boost_mysql_example_${EXAMPLE_NAME}") - - build_example(${EXECUTABLE_NAME} "${EXAMPLE_NAME}.cpp") - - if (BOOST_MYSQL_VALGRIND_TESTS AND DO_MEMCHECK) - memcheck_example(${EXECUTABLE_NAME} ${CONNECTION_ARG}) - else() - test_example(${EXECUTABLE_NAME} ${CONNECTION_ARG}) - endif() -endfunction() - # Get the MySQL hostname to use for examples if(DEFINED ENV{BOOST_MYSQL_SERVER_HOST}) set(SERVER_HOST $ENV{BOOST_MYSQL_SERVER_HOST}) @@ -64,20 +16,92 @@ else() set(SERVER_HOST "localhost") endif() +add_library(boost_mysql_examples_common INTERFACE) +target_link_libraries( + boost_mysql_examples_common + INTERFACE + Boost::disable_autolinking + Boost::mysql + boost_mysql_asio +) + +# Build and run an example +function (add_example EXAMPLE_NAME) + set(EXECUTABLE_NAME "boost_mysql_example_${EXAMPLE_NAME}") + add_executable(${EXECUTABLE_NAME} "${EXAMPLE_NAME}.cpp") + target_link_libraries(${EXECUTABLE_NAME} PRIVATE boost_mysql_examples_common) + common_target_settings(${EXECUTABLE_NAME}) + + if (BOOST_MYSQL_VALGRIND_TESTS) + add_memcheck_test( + NAME "${EXECUTABLE_NAME}_memcheck" + TARGET ${EXECUTABLE_NAME} + ARGUMENTS ${ARGN} + ) + else() + add_test( + NAME ${EXECUTABLE_NAME} + COMMAND ${EXECUTABLE_NAME} ${ARGN} + ) + endif() +endfunction() + +# Coroutines needs Boost.Context and shouldn't be memchecked +function (add_example_coroutines) + set(EXECUTABLE_NAME boost_mysql_example_async_coroutines) + add_executable(${EXECUTABLE_NAME} async_coroutines.cpp) + target_link_libraries( + ${EXECUTABLE_NAME} + PRIVATE + boost_mysql_examples_common + Boost::context + ) + common_target_settings(${EXECUTABLE_NAME}) + add_test( + NAME ${EXECUTABLE_NAME} + COMMAND ${EXECUTABLE_NAME} example_user example_password ${SERVER_HOST} + ) +endfunction() + +# The stored procedures example must be run several times through a Python script +function (add_example_stored_procedures) + set(EXECUTABLE_NAME boost_mysql_example_stored_procedures) + add_executable(${EXECUTABLE_NAME} stored_procedures.cpp) + target_link_libraries( + ${EXECUTABLE_NAME} + PRIVATE + boost_mysql_examples_common + ) + common_target_settings(${EXECUTABLE_NAME}) + add_test( + NAME ${EXECUTABLE_NAME} + COMMAND + python + ${CMAKE_CURRENT_SOURCE_DIR}/private/run_stored_procedures.py + $ + ${SERVER_HOST} + ) +endfunction() + # Build and run all the examples -add_example(snippets TRUE ${SERVER_HOST}) -add_example(tutorial TRUE ${SERVER_HOST}) -add_example(text_queries TRUE ${SERVER_HOST}) -add_example(prepared_statements TRUE ${SERVER_HOST}) -add_example(async_callbacks TRUE ${SERVER_HOST}) -add_example(async_coroutines FALSE ${SERVER_HOST}) -add_example(async_coroutinescpp20 TRUE ${SERVER_HOST}) -add_example(async_futures TRUE ${SERVER_HOST}) -add_example(default_completion_tokens TRUE ${SERVER_HOST}) -add_example(metadata TRUE ${SERVER_HOST}) -add_example(ssl TRUE ${SERVER_HOST}) -add_example(timeouts TRUE ${SERVER_HOST}) +add_example(snippets example_user example_password ${SERVER_HOST}) +add_example(tutorial example_user example_password ${SERVER_HOST}) +add_example(text_queries example_user example_password ${SERVER_HOST}) +add_example(prepared_statements example_user example_password ${SERVER_HOST}) +add_example(async_callbacks example_user example_password ${SERVER_HOST}) +add_example_coroutines() +add_example(async_coroutinescpp20 example_user example_password ${SERVER_HOST}) +add_example(async_futures example_user example_password ${SERVER_HOST}) +add_example(default_completion_tokens example_user example_password ${SERVER_HOST}) +add_example(metadata example_user example_password ${SERVER_HOST}) +add_example(ssl example_user example_password ${SERVER_HOST}) +add_example(timeouts example_user example_password ${SERVER_HOST}) +add_example(source_script example_user example_password ${SERVER_HOST} ${CMAKE_CURRENT_SOURCE_DIR}/private/test_script.sql) +add_example_stored_procedures() if ("$ENV{BOOST_MYSQL_NO_UNIX_SOCKET_TESTS}" STREQUAL "") - add_example(unix_socket TRUE "/var/run/mysqld/mysqld.sock") + add_example(unix_socket example_user example_password) endif() + + + diff --git a/example/Jamfile b/example/Jamfile index f822dfd0..c2c7c926 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -7,6 +7,8 @@ import os ; +path-constant this_dir : . ; + # The hostname to use for examples local hostname = [ os.environ BOOST_MYSQL_SERVER_HOST ] ; if $(hostname) = "" @@ -15,7 +17,7 @@ if $(hostname) = "" } # Example list -local examples_no_unix = +local regular_examples = snippets tutorial text_queries @@ -30,10 +32,7 @@ local examples_no_unix = timeouts ; -local example_targets ; - -# Non-UNIX -for local example in $(examples_no_unix) +for local example in $(regular_examples) { local example_name = "boost_mysql_example_$(example)" ; unit-test $(example_name) @@ -44,23 +43,36 @@ for local example in $(examples_no_unix) : "example_user example_password $(hostname)" ; - explicit $(example_name) ; - example_targets += $(example_name) ; } # UNIX. Honor BOOST_MYSQL_NO_UNIX_SOCKET_TESTS for homogeneity with cmake if [ os.environ BOOST_MYSQL_NO_UNIX_SOCKET_TESTS ] = "" { unit-test boost_mysql_example_unix_socket - : - unix_socket.cpp - /boost/mysql/test//mysql - : - "example_user example_password" - ; - explicit boost_mysql_example_unix_socket ; - example_targets += boost_mysql_example_unix_socket ; + : + unix_socket.cpp + /boost/mysql/test//mysql + : + "example_user example_password" + ; } -# Helper to run all of them in one go -alias boost_mysql_all_examples : $(example_targets) ; -explicit boost_mysql_all_examples ; +# Stored procedures. The example is a command line tool that must be run +# several times in order, so we do this through a Python script +unit-test boost_mysql_example_stored_procedures + : + stored_procedures.cpp + /boost/mysql/test//mysql + : requirements + "python $(this_dir)/private/run_stored_procedures.py" + $(hostname) + ; + + +# Source script. unit-test doesn't support input-file correctly +unit-test boost_mysql_example_source_script + : + source_script.cpp + /boost/mysql/test//mysql + : requirements + "example_user example_password $(hostname) $(this_dir)/private/test_script.sql" + ; diff --git a/example/db_setup.sql b/example/db_setup.sql index 1a7d200c..67156997 100644 --- a/example/db_setup.sql +++ b/example/db_setup.sql @@ -26,6 +26,11 @@ CREATE TABLE employee( company_id CHAR(10) NOT NULL, FOREIGN KEY (company_id) REFERENCES company(id) ); +CREATE TABLE audit_log( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + t TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + msg TEXT +); INSERT INTO company (name, id) VALUES ("Award Winning Company, Inc.", "AWC"), diff --git a/example/db_setup_stored_procedures.sql b/example/db_setup_stored_procedures.sql new file mode 100644 index 00000000..3c4e8233 --- /dev/null +++ b/example/db_setup_stored_procedures.sql @@ -0,0 +1,316 @@ +-- +-- Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +-- +-- Distributed under the Boost Software License, Version 1.0. (See accompanying +-- file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +-- + +-- Connection system variables +SET NAMES utf8; + +-- Database +DROP DATABASE IF EXISTS boost_mysql_stored_procedures; +CREATE DATABASE boost_mysql_stored_procedures; +USE boost_mysql_stored_procedures; + +-- User +DROP USER IF EXISTS 'sp_user'@'%'; +CREATE USER 'sp_user'@'%' IDENTIFIED WITH 'mysql_native_password'; +ALTER USER 'sp_user'@'%' IDENTIFIED BY 'sp_password'; +GRANT ALL PRIVILEGES ON boost_mysql_stored_procedures.* TO 'sp_user'@'%'; +FLUSH PRIVILEGES; + +-- Table definitions +CREATE TABLE products ( + id INT PRIMARY KEY AUTO_INCREMENT, + short_name VARCHAR(100) NOT NULL, + descr TEXT, + price INT NOT NULL, + FULLTEXT(short_name, descr) +); + +CREATE TABLE orders( + id INT PRIMARY KEY AUTO_INCREMENT, + `status` ENUM('draft', 'pending_payment', 'complete') NOT NULL DEFAULT 'draft' +); + +CREATE TABLE order_items( + id INT PRIMARY KEY AUTO_INCREMENT, + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); + +-- Procedures +DELIMITER // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE get_products(IN p_search VARCHAR(50)) +BEGIN + DECLARE max_products INT DEFAULT 20; + IF p_search IS NULL THEN + SELECT id, short_name, descr, price + FROM products + LIMIT max_products; + ELSE + SELECT id, short_name, descr, price FROM products + WHERE MATCH(short_name, descr) AGAINST(p_search) + LIMIT max_products; + END IF; +END // + +CREATE PROCEDURE create_order() +BEGIN + DECLARE new_order_id INT; + START TRANSACTION; + + -- Create the order + INSERT INTO orders () VALUES (); + SET new_order_id = LAST_INSERT_ID(); + + -- Return the order + SELECT + new_order_id AS id, + 'draft' AS `status`; + + COMMIT; +END // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE get_order( + IN p_order_id INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION READ ONLY; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'get_order: invalid parameters'; + END IF; + + -- Check that the order exists + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + + -- Return the order + SELECT + p_order_id AS id, + order_status AS `status`; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE get_orders() +BEGIN + SELECT id, `status` FROM orders; +END // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE add_line_item( + IN p_order_id INT, + IN p_product_id INT, + IN p_quantity INT, + OUT pout_line_item_id INT +) +BEGIN + DECLARE product_price INT; + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL OR p_product_id IS NULL OR p_quantity IS NULL OR p_quantity <= 0 THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'add_line_item: invalid params'; + END IF; + + -- Ensure that the product is valid + SELECT price INTO product_price FROM products WHERE id = p_product_id; + IF product_price IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given product does not exist'; + END IF; + + -- Get the order + SELECT `status` INTO order_status FROM orders WHERE id = p_order_id; + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given is not editable'; + END IF; + + -- Insert the new item + INSERT INTO order_items (order_id, product_id, quantity) VALUES (p_order_id, p_product_id, p_quantity); + + -- Return value + SET pout_line_item_id = LAST_INSERT_ID(); + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE remove_line_item( + IN p_line_item_id INT +) +BEGIN + DECLARE order_id INT; + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_line_item_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'remove_line_item: invalid params'; + END IF; + + -- Get the order + SELECT orders.id, orders.`status` + INTO order_id, order_status + FROM orders + JOIN order_items items ON (orders.id = items.order_id) + WHERE items.id = p_line_item_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order item does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not editable'; + END IF; + + -- Delete the line item + DELETE FROM order_items + WHERE id = p_line_item_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = order_id; + + COMMIT; +END // + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE checkout_order( + IN p_order_id INT, + OUT pout_order_total INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'checkout_order: invalid params'; + END IF; + + -- Get the order + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be checked out'; + END IF; + + -- Update the order + UPDATE orders SET `status` = 'pending_payment' WHERE id = p_order_id; + + -- Retrieve the total price + SELECT SUM(prod.price * item.quantity) + INTO pout_order_total + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS item_id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + + +CREATE DEFINER = 'sp_user'@'%' PROCEDURE complete_order( + IN p_order_id INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'complete_order: invalid params'; + END IF; + + -- Get the order + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'pending_payment' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be completed'; + END IF; + + -- Update the order + UPDATE orders SET `status` = 'complete' WHERE id = p_order_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS item_id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +DELIMITER ; + +-- Contents for the products table +INSERT INTO products (price, short_name, descr) VALUES + (6400, 'A Feast for Odin', 'A Feast for Odin is a points-driven game, with plethora of pathways to victory, with a range of risk balanced against reward. A significant portion of this is your central hall, which has a whopping -86 points of squares and a major part of your game is attempting to cover these up with various tiles. Likewise, long halls and island colonies can also offer large rewards, but they will have penalties of their own.'), + (1600, 'Railroad Ink', 'The critically acclaimed roll and write game where you draw routes on your board trying to connect the exits at its edges. The more you connect, the more points you make, but beware: each incomplete route will make you lose points!'), + (4000, 'Catan', 'Catan is a board game for two to four players in which you compete to gather resources and build the biggest settlements on the fictional island of Catan. It takes approximately one hour to play.'), + (2500, 'Not Alone', 'It is the 25th century. You are a member of an intergalactic expedition shipwrecked on a mysterious planet named Artemia. While waiting for the rescue ship, you begin to explore the planet but an alien entity picks up your scent and begins to hunt you. You are NOT ALONE! Will you survive the dangers of Artemia?'), + (4500, 'Dice Hospital', "In Dice Hospital, a worker placement board game, players are tasked with running a local hospital. Each round you'll be admitting new patients, hiring specialists, building new departments, and treating as many incoming patients as you can.") +; diff --git a/example/private/run_stored_procedures.py b/example/private/run_stored_procedures.py new file mode 100644 index 00000000..7f2c0697 --- /dev/null +++ b/example/private/run_stored_procedures.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +from subprocess import run, PIPE +import argparse +import re + + +def _parse_order_id(output: str) -> str: + res = re.search(r'Order: id=([0-9]*)', output) + assert res is not None + return res.group(1) + + +def _parse_line_item_id(output: str) -> str: + res = re.search(r'Created line item: id=([0-9]*)', output) + assert res is not None + return res.group(1) + + +class Runner: + def __init__(self, exe: str, host: str) -> None: + self._exe = exe + self._host = host + + def run(self, subcmd: str, *args: str) -> str: + cmdline = [self._exe, 'sp_user', 'sp_password', self._host, subcmd, *args] + print(' + ', cmdline) + res = run(cmdline, check=True, stdout=PIPE) + print(res.stdout.decode()) + return res.stdout.decode() + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('executable') + parser.add_argument('host') + args = parser.parse_args() + + runner = Runner(args.executable, args.host) + + runner.run('get-products', 'feast') + order_id = _parse_order_id(runner.run('create-order')) + runner.run('get-orders') + line_item_id = _parse_line_item_id(runner.run('add-line-item', order_id, '1', '5')) + runner.run('add-line-item', order_id, '2', '2') + runner.run('add-line-item', order_id, '3', '1') + runner.run('remove-line-item', line_item_id) + runner.run('get-order', order_id) + runner.run('checkout-order', order_id) + runner.run('complete-order', order_id) + runner.run('get-orders') + + +if __name__ == '__main__': + main() diff --git a/example/private/test_script.sql b/example/private/test_script.sql new file mode 100644 index 00000000..deec820c --- /dev/null +++ b/example/private/test_script.sql @@ -0,0 +1,17 @@ +-- +-- Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +-- +-- Distributed under the Boost Software License, Version 1.0. (See accompanying +-- file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +-- + +USE boost_mysql_examples; + +CREATE TEMPORARY TABLE products ( + id VARCHAR(50) PRIMARY KEY, + description VARCHAR(256) +); + +INSERT INTO products VALUES ('PTT', 'Potatoes'), ('CAR', NULL); +SELECT * FROM products; +DROP TABLE products; diff --git a/example/snippets.cpp b/example/snippets.cpp index a41f76a8..a536460d 100644 --- a/example/snippets.cpp +++ b/example/snippets.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -34,6 +36,7 @@ #include #include #include +#include #include #include @@ -73,6 +76,8 @@ using boost::mysql::tcp_ssl_connection; } const char* get_value_from_user() { return ""; } +std::int64_t get_employee_id() { return 42; } +std::string get_company_id() { return "HGS"; } //[prepared_statements_execute // description, price and show_in_store are not trusted, since they may @@ -448,6 +453,8 @@ void main_impl(int argc, char** argv) run_overview_coro(conn); } #endif + + // prepared statements { //[prepared_statements_prepare // Table setup @@ -477,6 +484,146 @@ void main_impl(int argc, char** argv) #endif conn.query("DROP TABLE products", result); } + + // multi-resultset + { + results result; + conn.query("DROP PROCEDURE IF EXISTS get_employee", result); + + //[multi_resultset_procedure + conn.query( + R"( + CREATE PROCEDURE get_employee(IN pin_employee_id INT) + BEGIN + SELECT * FROM employee WHERE id = pin_employee_id; + END + )", + result + ); + //] + + //[multi_resultset_call + // The procedure parameter, employe_id, will likely be obtained from an untrusted source, + // so we will use a prepared statement + statement get_employee_stmt = conn.prepare_statement("CALL get_employee(?)"); + + // Obtain the parameters required to call the statement, e.g. from a file or HTTP message + std::int64_t employee_id = get_employee_id(); + + // Call the statement + conn.execute_statement(get_employee_stmt, std::make_tuple(employee_id), result); + //] + + //[multi_resultset_first_resultset + rows_view matched_employees = result.at(0).rows(); + // Use matched_employees as required + //] + + boost::ignore_unused(matched_employees); + } + { + results result; + conn.query("DROP PROCEDURE IF EXISTS create_employee", result); + + //[multi_resultset_out_params + // Setup the stored procedure + conn.query( + R"( + CREATE PROCEDURE create_employee( + IN pin_company_id CHAR(10), + IN pin_first_name VARCHAR(100), + IN pin_last_name VARCHAR(100), + OUT pout_employee_id INT + ) + BEGIN + START TRANSACTION; + INSERT INTO employee (company_id, first_name, last_name) + VALUES (pin_company_id, pin_first_name, pin_last_name); + SET pout_employee_id = LAST_INSERT_ID(); + INSERT INTO audit_log (msg) VALUES ('Created new employee...'); + COMMIT; + END + )", + result + ); + + // To retrieve output parameters, you must use prepared statements. Text queries don't support this + // We specify placeholders for both IN and OUT parameters + statement stmt = conn.prepare_statement("CALL create_employee(?, ?, ?, ?)"); + + // When executing the statement, we provide an actual value for the IN parameters, + // and a dummy value for the OUT parameter. This value will be ignored, but it's required by the + // protocol + conn.execute_statement(stmt, std::make_tuple("HGS", "John", "Doe", nullptr), result); + + // Retrieve output parameters. This row_view has an element per + // OUT or INOUT parameter that used a ? placeholder + row_view output_params = result.out_params(); + std::int64_t new_employee_id = output_params.at(0).as_int64(); + //] + + boost::ignore_unused(new_employee_id); + } + { + boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); + auto endpoint = *resolver.resolve(argv[3], boost::mysql::default_port_string).begin(); + + //[multi_resultset_multi_queries + // The username and password to use + boost::mysql::handshake_params params( + argv[1], // username + argv[2], // password + "boost_mysql_examples" // database + ); + + // Allows running multiple semicolon-separated in a single call. + // We must set this before calling connect + params.set_multi_queries(true); + + // Connect to the server specifying that we want support for multi-queries + conn.connect(endpoint, params); + + // We can now use the multi-query feature. + // This will result in three resultsets, one per query. + results result; + conn.query( + R"( + CREATE TEMPORARY TABLE posts ( + id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR (256), + body TEXT + ); + INSERT INTO posts (title, body) VALUES ('Breaking news', 'Something happened!'); + SELECT COUNT(*) FROM posts; + )", + result + ); + //] + + //[multi_resultset_results_as_collection + // result is actually a random-access collection of resultsets. + // The INSERT is the 2nd query, so we can access its resultset like this: + boost::mysql::resultset_view insert_result = result.at(1); + + // A resultset has metadata, rows, and additional data, like the last insert ID: + std::int64_t post_id = insert_result.last_insert_id(); + + // The SELECT result is the third one, so we can access it like this: + boost::mysql::resultset_view select_result = result.at(2); + + // select_result is a view that points into result. + // We can take ownership of it using the resultse class: + boost::mysql::resultset owning_select_result(select_result); // valid even after result is destroyed + + // We can access rows of resultset objects as usual: + std::int64_t num_posts = owning_select_result.rows().at(0).at(0).as_int64(); + //] + + boost::ignore_unused(post_id); + boost::ignore_unused(num_posts); + } + + // multi-function { //[multi_function_setup results result; @@ -541,6 +688,62 @@ void main_impl(int argc, char** argv) read_all_rows(st); // don't compromise further operations conn.query("DROP TABLE posts", result); } + + { + results result; + conn.query("DROP PROCEDURE IF EXISTS get_company", result); + + //[multi_function_stored_procedure + // Setup the stored procedure + conn.query( + R"( + CREATE PROCEDURE get_company(IN pin_company_id CHAR(10)) + BEGIN + START TRANSACTION READ ONLY; + SELECT * FROM company WHERE id = pin_company_id; + SELECT * FROM employee WHERE company_id = pin_company_id; + COMMIT; + END + )", + result + ); + + // Get the company ID to retrieve, possibly from the user + std::string company_id = get_company_id(); + + // Call the procedure + execution_state st; + statement stmt = conn.prepare_statement("CALL get_company(?)"); + conn.start_statement_execution(stmt, std::make_tuple(company_id), st); + + // The above code will generate 3 resultsets + // Read the 1st one, which contains the matched companies + while (st.should_read_rows()) + { + rows_view company_batch = conn.read_some_rows(st); + // Use the retrieved companies as required + //<- + boost::ignore_unused(company_batch); + //-> + } + + // Move on to the 2nd one, containing the employees for these companies + conn.read_resultset_head(st); + while (st.should_read_rows()) + { + rows_view employee_batch = conn.read_some_rows(st); + // Use the retrieved employees as required + //<- + boost::ignore_unused(employee_batch); + //-> + } + + // The last one is an empty resultset containing information about the + // CALL statement itself. We're not interested in this + conn.read_resultset_head(st); + assert(st.complete()); + //] + } } // fields diff --git a/example/source_script.cpp b/example/source_script.cpp new file mode 100644 index 00000000..3686ca1e --- /dev/null +++ b/example/source_script.cpp @@ -0,0 +1,185 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +//[example_source_script + +#include + +#include +#include +#include + +#include +#include +#include +#include + +/** + * This example runs all command in a .sql file, using multi-queries. + * Note that special commands that are handled by the mysql command line tool + * (like DELIMITER) won't work. + * + * For this example, we will be using the 'boost_mysql_examples' database. + * You can get this database by running db_setup.sql. + * This example assumes you are connecting to a localhost MySQL server. + * + * This example uses synchronous functions and handles errors using exceptions. + */ + +// Reads a file into memory +std::string read_file(const char* file_name) +{ + std::ifstream ifs(file_name); + if (!ifs) + throw std::runtime_error("Cannot open file: " + std::string(file_name)); + return std::string(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); +} + +void print_column_names(boost::mysql::metadata_collection_view meta_collection) +{ + if (meta_collection.empty()) + return; + + bool is_first = true; + for (auto meta : meta_collection) + { + if (!is_first) + { + std::cout << " | "; + } + is_first = false; + std::cout << meta.column_name(); + } + std::cout << "\n-----------------\n"; +} + +void print_row(boost::mysql::row_view row) +{ + bool is_first = true; + for (auto field : row) + { + if (!is_first) + { + std::cout << " | "; + } + is_first = false; + std::cout << field; + } + std::cout << '\n'; +} + +void print_ok(const boost::mysql::execution_state& st) +{ + std::cout << "Affected rows: " << st.affected_rows() + << "\n" + "Last insert ID: " + << st.last_insert_id() + << "\n" + "Warnings: " + << st.warning_count() << "\n\n" + << std::flush; +} + +void main_impl(int argc, char** argv) +{ + if (argc != 5) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + // Read the script file into memory + std::string script_contents = read_file(argv[4]); + + // Set up the io_context, SSL context and connection required to + // connect to the server. + boost::asio::io_context ctx; + boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); + boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); + + // Resolve the server hostname to get a collection of endpoints + boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); + auto endpoints = resolver.resolve(argv[3], boost::mysql::default_port_string); + + // The username, password and database to use + boost::mysql::handshake_params params( + argv[1], // username + argv[2], // password + "boost_mysql_examples" // database + ); + + // We're going to use multi-queries, which enables passing the server + // a set of semicolon-separated queries. We need to explicitly enable support for it. + params.set_multi_queries(true); + + // We'll be using metadata strings to print column names, so we need to enable support for it + conn.set_meta_mode(boost::mysql::metadata_mode::full); + + // Connect to the server using the first endpoint returned by the resolver + conn.connect(*endpoints.begin(), params); + + // The executed commands may generate a lot of output, so we're going to + // use multi-function operations (i.e. start_query) to read it in batches. + boost::mysql::execution_state st; + conn.start_query(script_contents, st); + + // The main read loop. Each executed command will yield a resultset. + // st.comoplete() returns true once all resultsets have been read. + for (std::size_t resultset_number = 0; !st.complete(); ++resultset_number) + { + // Advance to next resultset, if required + if (st.should_read_head()) + { + conn.read_resultset_head(st); + } + + // Print the name of the fields + std::cout << "Resultset number " << resultset_number << "\n"; + print_column_names(st.meta()); + + // Read the rows and print them + while (st.should_read_rows()) + { + boost::mysql::rows_view batch = conn.read_some_rows(st); + for (auto row : batch) + { + print_row(row); + } + } + + // Print OK packet data + print_ok(st); + } + + // Close the connection + conn.close(); +} + +int main(int argc, char** argv) +{ + try + { + main_impl(argc, argv); + } + catch (const boost::mysql::error_with_diagnostics& err) + { + // 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 encoding + // (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; + } +} + +//] diff --git a/example/stored_procedures.cpp b/example/stored_procedures.cpp new file mode 100644 index 00000000..5c8a36f4 --- /dev/null +++ b/example/stored_procedures.cpp @@ -0,0 +1,480 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +//[example_stored_procedures + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +/** + * This example implements a ver simple command-line order manager + * for an online store, using stored procedures. You can find the procedure + * definitions in example/db_setup_stored_procedures.sql. Be sure to run this file before the example. + * This example assumes you are connecting to a localhost MySQL server. + * + * The order system is intentionally very simple, and has the following tables: + * - products: the list of items our store sells, with price and description. + * - orders: the main object. Orders have a status field that can be draft, pending_payment or complete. + * - order_items: an order may have 0 to n line items. Each item refers to a single product. + * + * Orders are created empty, in a draft state. Line items can be added or removed. + * Orders are then checked out, which transitions them to pending_payment. + * After that, payment would happen through an external system. Once completed, an + * order is confirmed, transitioning it to the complete status. + * In the real world, flow would be much more complex, but this is enough for an example. + */ + +using boost::mysql::resultset_view; +using boost::mysql::row_view; +using boost::mysql::rows_view; +using boost::mysql::string_view; + +namespace { + +/** + * Our command line tool implements several sub-commands. Each sub-command + * has a set of arguments. We define a struct for each sub-command. + */ + +struct get_products_args +{ + std::string search; +}; + +struct create_order_args +{ +}; + +struct get_order_args +{ + std::int64_t order_id; +}; + +struct get_orders_args +{ +}; + +struct add_line_item_args +{ + std::int64_t order_id; + std::int64_t product_id; + std::int64_t quantity; +}; + +struct remove_line_item_args +{ + std::int64_t line_item_id; +}; + +struct checkout_order_args +{ + std::int64_t order_id; +}; + +struct complete_order_args +{ + std::int64_t order_id; +}; + +// A variant type that can represent arguments for any of the sub-commands +using any_command = boost::variant2::variant< + get_products_args, + get_order_args, + get_orders_args, + create_order_args, + add_line_item_args, + remove_line_item_args, + checkout_order_args, + complete_order_args>; + +// In-memory representation of the command-line arguments once parsed. +struct cmdline_args +{ + const char* username; + const char* password; + const char* host; + any_command cmd; +}; + +// Call on error to print usage and exit +[[noreturn]] void usage(string_view program_name) +{ + std::cerr << "Usage: " << program_name << " args...\n" + << "Available commands:\n" + " get-products \n" + " create-order\n" + " get-order \n" + " get-orders\n" + " add-line-item \n" + " remove-line-item \n" + " checkout-order \n" + " complete-order " + << std::endl; + exit(1); +} + +// Helper function to parse a sub-command +any_command parse_subcommand(string_view program_name, string_view cmd_name, int argc_rest, char** argv_rest) +{ + if (cmd_name == "get-products") + { + if (argc_rest != 1) + { + usage(program_name); + } + return get_products_args{argv_rest[0]}; + } + else if (cmd_name == "create-order") + { + if (argc_rest != 0) + { + usage(program_name); + } + return create_order_args{}; + } + else if (cmd_name == "get-order") + { + if (argc_rest != 1) + { + usage(program_name); + } + return get_order_args{std::stoi(argv_rest[0])}; + } + else if (cmd_name == "get-orders") + { + if (argc_rest != 0) + { + usage(program_name); + } + return get_orders_args{}; + } + else if (cmd_name == "add-line-item") + { + if (argc_rest != 3) + { + usage(program_name); + } + return add_line_item_args{ + std::stoi(argv_rest[0]), + std::stoi(argv_rest[1]), + std::stoi(argv_rest[2]), + }; + } + else if (cmd_name == "remove-line-item") + { + if (argc_rest != 1) + { + usage(program_name); + } + return remove_line_item_args{ + std::stoi(argv_rest[0]), + }; + } + else if (cmd_name == "checkout-order") + { + if (argc_rest != 1) + { + usage(program_name); + } + return checkout_order_args{std::stoi(argv_rest[0])}; + } + else if (cmd_name == "complete-order") + { + if (argc_rest != 1) + { + usage(program_name); + } + return complete_order_args{std::stoi(argv_rest[0])}; + } + else + { + usage(program_name); + } +} + +// Parses the entire command line +cmdline_args parse_cmdline_args(int argc, char** argv) +{ + if (argc < 5) + { + usage(argv[0]); + } + return cmdline_args{ + argv[1], + argv[2], + argv[3], + parse_subcommand(argv[0], argv[4], argc - 5, argv + 5), + }; +} + +/** + * We'll be using the variant using visit(). + * This visitor executes a sub-command and prints the results to stdout. + */ +struct visitor +{ + boost::mysql::tcp_ssl_connection& conn; + + // Prints the details of an order to stdout. An order here is represented as a row + static void print_order(row_view order) + { + std::cout << "Order: id=" << order.at(0) << ", status=" << order.at(1) << '\n'; + } + + // Prints the details of an order line item, again represented as a row + static void print_line_item(row_view item) + { + std::cout << " Line item: id=" << item.at(0) << ", quantity=" << item.at(1) + << ", unit_price=" << item.at(2).as_int64() / 100.0 << "$\n"; + } + + // Procedures that manipulate orders return two resultsets: one describing + // the order and another with the line items the order has. Some of them + // return only the order resultset. These functions print order details to stdout + static void print_order_with_items(resultset_view order_resultset, resultset_view line_items_resultset) + { + // First resultset: order information. Always a single row + print_order(order_resultset.rows().at(0)); + + // Second resultset: all order line items + rows_view line_items = line_items_resultset.rows(); + if (line_items.empty()) + { + std::cout << "No line items\n"; + } + else + { + for (row_view item : line_items) + { + print_line_item(item); + } + } + } + + // get-products : full text search of the products table + void operator()(const get_products_args& args) const + { + // We need to pass user-supplied params to CALL, so we use a statement + auto stmt = conn.prepare_statement("CALL get_products(?)"); + + boost::mysql::results result; + conn.execute_statement(stmt, std::make_tuple(args.search), result); + auto products = result.front(); + std::cout << "Your search returned the following products:\n"; + for (auto product : products.rows()) + { + std::cout << "* ID: " << product.at(0) << '\n' + << " Short name: " << product.at(1) << '\n' + << " Description: " << product.at(2) << '\n' + << " Price: " << product.at(3).as_int64() / 100.0 << "$" << std::endl; + } + std::cout << std::endl; + } + + // create-order: creates a new order + void operator()(const create_order_args&) const + { + // Since create_order doesn't have user-supplied params, we can use query() + boost::mysql::results result; + conn.query("CALL create_order()", result); + + // Print the result to stdout. create_order() returns a resultset for + // the newly created order, with only 1 row. + std::cout << "Created order\n"; + print_order(result.at(0).rows().at(0)); + } + + // get-order : retrieves order details + void operator()(const get_order_args& args) const + { + // The order_id is supplied by the user, so we use a prepared statement + auto stmt = conn.prepare_statement("CALL get_order(?)"); + + // Execute the statement + boost::mysql::results result; + conn.execute_statement(stmt, std::make_tuple(args.order_id), result); + + // Print the result to stdout. get_order() returns a resultset for + // the retrieved order and another for the line items. If the order can't + // be found, get_order() raises an error using SIGNAL, which will make + // execute_statement() fail with an exception. + std::cout << "Retrieved order\n"; + print_order_with_items(result.at(0), result.at(1)); + } + + // get-orders: lists all orders + void operator()(const get_orders_args&) const + { + // Since get_orders doesn't have user-supplied params, we can use query() + boost::mysql::results result; + conn.query("CALL get_orders()", result); + + // Print results to stdout. get_orders() succeeds even if no order is found. + // get_orders() only lists orders, not line items. + rows_view orders = result.front().rows(); + if (orders.empty()) + { + std::cout << "No orders found" << std::endl; + } + else + { + for (row_view order : result.front().rows()) + { + print_order(order); + } + } + } + + // add-line-item : adds a line item to a given order + void operator()(const add_line_item_args& args) const + { + // add_line_item has several user-supplied arguments, so we must use a statement. + // The 4th argument is an OUT parameter. If we bind it by passing a ? marker, + // we will get an extra resultset with just its value. + auto stmt = conn.prepare_statement("CALL add_line_item(?, ?, ?, ?)"); + + // We still have to pass a value to the 4th argument, even if it's an OUT parameter. + // The value will be ignored, so we can pass nullptr. + boost::mysql::results result; + conn.execute_statement( + stmt, + std::make_tuple(args.order_id, args.product_id, args.quantity, nullptr), + result + ); + + // We can use results::out_params() to access the extra resultset containing + // the OUT parameter + auto new_line_item_id = result.out_params().at(0).as_int64(); + + // Print the results to stdout + std::cout << "Created line item: id=" << new_line_item_id << "\n"; + print_order_with_items(result.at(0), result.at(1)); + } + + // remove-line-item : removes an item from an order + void operator()(const remove_line_item_args& args) const + { + // remove_line_item has user-supplied parameters, so we use a statement + auto stmt = conn.prepare_statement("CALL remove_line_item(?)"); + + // Run the procedure + boost::mysql::results result; + conn.execute_statement(stmt, std::make_tuple(args.line_item_id), result); + + // Print results to stdout + std::cout << "Removed line item from order\n"; + print_order_with_items(result.at(0), result.at(1)); + } + + // checkout-order : marks an order as ready for checkout + void operator()(const checkout_order_args& args) const + { + // checkout_order has user-supplied parameters, so we use a statement. + // The 2nd parameter represents the total order amount and is an OUT parameter. + auto stmt = conn.prepare_statement("CALL checkout_order(?, ?)"); + + // Execute the statement + boost::mysql::results result; + conn.execute_statement(stmt, std::make_tuple(args.order_id, nullptr), result); + + // We can use results::out_params() to access the extra resultset containing + // the OUT parameter + auto total_amount = result.out_params().at(0).as_int64(); + + // Print the results to stdout + std::cout << "Checked out order. The total amount to pay is: " << total_amount / 100.0 << "$\n"; + print_order_with_items(result.at(0), result.at(1)); + } + + // complete-order : marks an order as completed + void operator()(const complete_order_args& args) const + { + // complete_order has user-supplied parameters, so we use a statement. + auto stmt = conn.prepare_statement("CALL complete_order(?)"); + + // Execute the statement + boost::mysql::results result; + conn.execute_statement(stmt, std::make_tuple(args.order_id), result); + + // Print the results to stdout + std::cout << "Completed order\n"; + print_order_with_items(result.at(0), result.at(1)); + } +}; + +void main_impl(int argc, char** argv) +{ + // Parse command line arguments + auto args = parse_cmdline_args(argc, argv); + + // I/O context and connection. We use SSL because MySQL 8+ default settings require it. + boost::asio::io_context ctx; + boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); + boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); + + // Resolver for hostname resolution + boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); + + // Connection params + boost::mysql::handshake_params params( + args.username, // username + args.password, // password + "boost_mysql_stored_procedures" // database to use + ); + + // Hostname resolution + auto endpoints = resolver.resolve(args.host, boost::mysql::default_port_string); + + // TCP and MySQL level connect + conn.connect(*endpoints.begin(), params); + + // Execute the command + boost::variant2::visit(visitor{conn}, args.cmd); + + // Close the connection + conn.close(); +} + +} // namespace + +int main(int argc, char** argv) +{ + try + { + main_impl(argc, argv); + } + catch (const boost::mysql::error_with_diagnostics& err) + { + // Some errors include additional diagnostics, like server-provided error messages. + // If a store procedure fails (e.g. because a SIGNAL statement was executed), an error + // like this will be raised. + // 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 encoding + // (UTF-8 by default). Treat is as untrusted input. + std::cerr << "Error: " << err.what() << ", error code: " << err.code() << '\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; + } +} + +//] diff --git a/include/boost/mysql/connection.hpp b/include/boost/mysql/connection.hpp index dae2f77c..fba6d4e5 100644 --- a/include/boost/mysql/connection.hpp +++ b/include/boost/mysql/connection.hpp @@ -364,13 +364,13 @@ public: * \brief Starts a text query as a multi-function operation. * \details * Writes the query request and reads the initial server response and the column - * metadata, but not the generated rows, if any. After this operation completes, `st` will have - * \ref execution_state::meta populated, and may become \ref execution_state::complete - * if the operation did not generate any rows (e.g. it was an `UPDATE`). + * metadata, but not the generated rows or subsequent resultsets, if any. + * After this operation completes, `st` will have + * \ref execution_state::meta populated. * Metadata will be populated according to `this->meta_mode()`. * \n - * If the operation generated any rows, these must be read (by using - * \ref read_some_rows) before engaging in any further network operation. + * If the operation generated any rows or more than one resultset, these must be read (by using + * \ref read_some_rows and \ref read_resultset_head) before engaging in any further network operation. * Otherwise, the results are undefined. * \n * `query_string` should be encoded using the connection's character set. @@ -545,14 +545,13 @@ public: * \brief Starts a statement execution as a multi-function operation. * \details * Writes the execute request and reads the initial server response and the column - * metadata, but not the generated rows, if any. After this operation completes, `st` will have - * \ref execution_state::meta populated, and may become \ref execution_state::complete - * if the operation did not generate any rows (e.g. it was an `UPDATE`). - * Metadata will be populated according to `this->meta_mode()`. + * metadata, but not the generated rows or subsequent resultsets, if any. After this operation completes, + * `st` will have \ref execution_state::meta populated. Metadata will be populated according to + * `this->meta_mode()`. * \n - * If the operation generated any rows, these must be read (by using - * \ref read_some_rows) before engaging in any further - * operation involving server communication. Otherwise, the results are undefined. + * If the operation generated any rows or more than one resultset, these must be read (by using + * \ref read_some_rows and \ref read_resultset_head) before engaging in any further network operation. + * Otherwise, the results are undefined. * \n * The statement actual parameters (`params`) are passed as a `std::tuple` of elements. * String parameters should be encoded using the connection's character set. @@ -630,13 +629,12 @@ public: * \brief Starts a statement execution as a multi-function operation. * \details * Writes the execute request and reads the initial server response and the column - * metadata, but not the generated rows, if any. After this operation completes, `st` will have - * \ref execution_state::meta populated, and may become \ref execution_state::complete - * if the operation did not generate any rows (e.g. it was an `UPDATE`). + * metadata, but not the generated rows or any subsequent resultsets, if any. After this operation + * completes, `st` will have \ref execution_state::meta populated. * \n - * If the operation generated any rows, these must be read (by using - * \ref connection::read_some_rows) before engaging in any further - * operation involving server communication. Otherwise, the results are undefined. + * If the operation generated any rows or more than one resultset, these must be read (by using + * \ref read_some_rows and \ref read_resultset_head) before engaging in any further network operation. + * Otherwise, the results are undefined. * \n * The statement actual parameters are passed as an iterator range. * String parameters should be encoded using the connection's character set. @@ -757,9 +755,9 @@ public: /** * \brief Reads a batch of rows. * \details - * The number of rows that will be read is unspecified. If the resultset being read - * has still rows to read, at least one will be read. If there are no more - * rows to be read, returns an empty `rows_view`. + * The number of rows that will be read is unspecified. If the operation represented by `st` + * has still rows to read, at least one will be read. If there are no more rows, or + * `st.should_read_rows() == false`, returns an empty `rows_view`. * \n * The number of rows that will be read depends on the input buffer size. The bigger the buffer, * the greater the batch size (up to a maximum). You can set the initial buffer size in `connection`'s @@ -802,6 +800,51 @@ public: CompletionToken&& token BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type) ); + /** + * \brief Reads metadata for subsequent resultsets in a multi-resultset operation. + * \details + * If `st.should_read_head() == true`, this function will read the next resultset's + * initial response message and metadata, if any. If the resultset indicates a failure + * (e.g. the query associated to this resultset contained an error), this function will fail + * with that error. + * \n + * If `st.should_read_head() == false`, this function is a no-op. + * \n + * This function is only relevant when using multi-function operations with statements + * that return more than one resultset. + */ + void read_resultset_head(execution_state& st, error_code& err, diagnostics& info); + + /// \copydoc read_resultset_head + void read_resultset_head(execution_state& st); + + /** + * \copydoc read_resultset_head + * \par Handler signature + * The handler signature for this operation is + * `void(boost::mysql::error_code)`. + */ + template + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) + async_read_resultset_head( + execution_state& st, + CompletionToken&& token BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type) + ) + { + return async_read_resultset_head(st, shared_diag(), std::forward(token)); + } + + /// \copydoc async_read_resultset_head + template + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) + async_read_resultset_head( + execution_state& st, + diagnostics& diag, + CompletionToken&& token BOOST_ASIO_DEFAULT_COMPLETION_TOKEN(executor_type) + ); + /** * \brief Checks whether the server is alive. * \details diff --git a/include/boost/mysql/detail/auxiliar/access_fwd.hpp b/include/boost/mysql/detail/auxiliar/access_fwd.hpp index b11260ca..b6b83d5b 100644 --- a/include/boost/mysql/detail/auxiliar/access_fwd.hpp +++ b/include/boost/mysql/detail/auxiliar/access_fwd.hpp @@ -20,9 +20,9 @@ struct execution_state_access; struct field_view_access; struct metadata_access; struct results_access; +struct resultset_view_access; struct row_view_access; struct rows_view_access; -struct rows_access; struct statement_access; } // namespace detail diff --git a/include/boost/mysql/detail/auxiliar/impl/row_base.ipp b/include/boost/mysql/detail/auxiliar/impl/row_base.ipp deleted file mode 100644 index 1b418380..00000000 --- a/include/boost/mysql/detail/auxiliar/impl/row_base.ipp +++ /dev/null @@ -1,120 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_AUXILIAR_IMPL_ROW_BASE_IPP -#define BOOST_MYSQL_DETAIL_AUXILIAR_IMPL_ROW_BASE_IPP - -#pragma once - -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -inline bool overlaps(const void* first1, const void* first2, std::size_t size) noexcept -{ - std::greater_equal gte; - std::less lt; - const void* last1 = static_cast(first1) + size; - const void* last2 = static_cast(first2) + size; - return (gte(first1, first2) && lt(first1, last2)) || (gte(last1, first2) && lt(last1, last2)); -} - -inline std::size_t get_string_size(field_view f) noexcept -{ - switch (f.kind()) - { - case field_kind::string: return f.get_string().size(); - case field_kind::blob: return f.get_blob().size(); - default: return 0; - } -} - -inline unsigned char* copy_string(unsigned char* buffer_it, field_view& f) noexcept -{ - auto str = f.get_string(); - if (!str.empty()) - { - assert(!overlaps(buffer_it, str.data(), str.size())); - std::memcpy(buffer_it, str.data(), str.size()); - f = field_view(string_view(reinterpret_cast(buffer_it), str.size())); - buffer_it += str.size(); - } - return buffer_it; -} - -inline unsigned char* copy_blob(unsigned char* buffer_it, field_view& f) noexcept -{ - auto b = f.get_blob(); - if (!b.empty()) - { - assert(!overlaps(buffer_it, b.data(), b.size())); - std::memcpy(buffer_it, b.data(), b.size()); - f = field_view(blob_view(buffer_it, b.size())); - buffer_it += b.size(); - } - return buffer_it; -} - -} // namespace detail -} // namespace mysql -} // namespace boost - -boost::mysql::detail::row_base::row_base(const field_view* fields, std::size_t size) - : fields_(fields, fields + size) -{ - copy_strings(); -} - -boost::mysql::detail::row_base::row_base(const row_base& rhs) : fields_(rhs.fields_) { copy_strings(); } - -boost::mysql::detail::row_base& boost::mysql::detail::row_base::operator=(const row_base& rhs) -{ - if (this != &rhs) - { - fields_ = rhs.fields_; - copy_strings(); - } - return *this; -} - -void boost::mysql::detail::row_base::assign(const field_view* fields, std::size_t size) -{ - fields_.assign(fields, fields + size); - copy_strings(); -} - -inline void boost::mysql::detail::row_base::copy_strings() -{ - // Calculate the required size for the new strings - std::size_t size = 0; - for (auto f : fields_) - { - size += get_string_size(f); - } - - // Make space - string_buffer_.resize(size); - - // Copy strings and blobs - unsigned char* buffer_it = string_buffer_.data(); - for (auto& f : fields_) - { - switch (f.kind()) - { - case field_kind::string: buffer_it = copy_string(buffer_it, f); break; - case field_kind::blob: buffer_it = copy_blob(buffer_it, f); break; - default: break; - } - } - assert(buffer_it == string_buffer_.data() + string_buffer_.size()); -} - -#endif diff --git a/include/boost/mysql/detail/auxiliar/impl/row_impl.ipp b/include/boost/mysql/detail/auxiliar/impl/row_impl.ipp new file mode 100644 index 00000000..f5e23104 --- /dev/null +++ b/include/boost/mysql/detail/auxiliar/impl/row_impl.ipp @@ -0,0 +1,197 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_AUXILIAR_IMPL_ROW_IMPL_IPP +#define BOOST_MYSQL_DETAIL_AUXILIAR_IMPL_ROW_IMPL_IPP + +#pragma once + +#include +#include +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +inline bool overlaps(const void* first1, const void* first2, std::size_t size) noexcept +{ + std::greater_equal gte; + std::less lt; + const void* last1 = static_cast(first1) + size; + const void* last2 = static_cast(first2) + size; + return (gte(first1, first2) && lt(first1, last2)) || (gte(last1, first2) && lt(last1, last2)); +} + +inline void guarded_memcpy(void* to, const void* from, std::size_t size) noexcept +{ + assert(!overlaps(to, from, size)); + std::memcpy(to, from, size); +} + +inline std::size_t get_string_size(field_view f) noexcept +{ + switch (f.kind()) + { + case field_kind::string: return f.get_string().size(); + case field_kind::blob: return f.get_blob().size(); + default: return 0; + } +} + +inline unsigned char* copy_string(unsigned char* buffer_it, field_view& f) noexcept +{ + auto str = f.get_string(); + if (!str.empty()) + { + guarded_memcpy(buffer_it, str.data(), str.size()); + f = field_view(string_view(reinterpret_cast(buffer_it), str.size())); + buffer_it += str.size(); + } + return buffer_it; +} + +inline unsigned char* copy_blob(unsigned char* buffer_it, field_view& f) noexcept +{ + auto b = f.get_blob(); + if (!b.empty()) + { + guarded_memcpy(buffer_it, b.data(), b.size()); + f = field_view(blob_view(buffer_it, b.size())); + buffer_it += b.size(); + } + return buffer_it; +} + +inline std::size_t copy_string_as_offset( + unsigned char* buffer_first, + std::size_t offset, + field_view& f +) noexcept +{ + auto str = f.get_string(); + if (!str.empty()) + { + guarded_memcpy(buffer_first + offset, str.data(), str.size()); + f = field_view_access::construct(string_view_offset(offset, str.size()), false); + return str.size(); + } + return 0; +} + +inline std::size_t copy_blob_as_offset( + unsigned char* buffer_first, + std::size_t offset, + field_view& f +) noexcept +{ + auto str = f.get_blob(); + if (!str.empty()) + { + guarded_memcpy(buffer_first + offset, str.data(), str.size()); + f = field_view_access::construct(string_view_offset(offset, str.size()), true); + return str.size(); + } + return 0; +} + +} // namespace detail +} // namespace mysql +} // namespace boost + +boost::mysql::detail::row_impl::row_impl(const field_view* fields, std::size_t size) + : fields_(fields, fields + size) +{ + copy_strings(); +} + +boost::mysql::detail::row_impl::row_impl(const row_impl& rhs) : fields_(rhs.fields_) { copy_strings(); } + +boost::mysql::detail::row_impl& boost::mysql::detail::row_impl::operator=(const row_impl& rhs) +{ + assign(rhs.fields_.data(), rhs.fields_.size()); + return *this; +} + +void boost::mysql::detail::row_impl::assign(const field_view* fields, std::size_t size) +{ + // Protect against self-assignment. This is valid as long as we + // don't implement sub-range operators (e.g. row_view[2:4]) + if (fields_.data() == fields) + { + assert(fields_.size() == size); + } + else + { + fields_.assign(fields, fields + size); + string_buffer_.clear(); + copy_strings(); + } +} + +inline void boost::mysql::detail::row_impl::copy_strings() +{ + // Calculate the required size for the new strings + std::size_t size = 0; + for (auto f : fields_) + { + size += get_string_size(f); + } + + // Make space. The previous fields should be in offset form + string_buffer_.resize(string_buffer_.size() + size); + + // Copy strings and blobs + unsigned char* buffer_it = string_buffer_.data(); + for (auto& f : fields_) + { + switch (f.kind()) + { + case field_kind::string: buffer_it = copy_string(buffer_it, f); break; + case field_kind::blob: buffer_it = copy_blob(buffer_it, f); break; + default: break; + } + } + assert(buffer_it == string_buffer_.data() + size); +} + +inline void boost::mysql::detail::row_impl::copy_strings_as_offsets(std::size_t first, std::size_t num_fields) +{ + // Preconditions + assert(first <= fields_.size()); + assert(first + num_fields <= fields_.size()); + + // Calculate the required size for the new strings + std::size_t size = 0; + for (std::size_t i = first; i < first + num_fields; ++i) + { + size += get_string_size(fields_[i]); + } + + // Make space. The previous fields should be in offset form + std::size_t old_string_buffer_size = string_buffer_.size(); + string_buffer_.resize(old_string_buffer_size + size); + + // Copy strings and blobs + std::size_t offset = old_string_buffer_size; + for (std::size_t i = first; i < first + num_fields; ++i) + { + auto& f = fields_[i]; + switch (f.kind()) + { + case field_kind::string: offset += copy_string_as_offset(string_buffer_.data(), offset, f); break; + case field_kind::blob: offset += copy_blob_as_offset(string_buffer_.data(), offset, f); break; + default: break; + } + } + assert(offset == string_buffer_.size()); +} + +#endif diff --git a/include/boost/mysql/detail/auxiliar/results_iterator.hpp b/include/boost/mysql/detail/auxiliar/results_iterator.hpp new file mode 100644 index 00000000..9f7961b0 --- /dev/null +++ b/include/boost/mysql/detail/auxiliar/results_iterator.hpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_AUXILIAR_RESULTS_ITERATOR_HPP +#define BOOST_MYSQL_DETAIL_AUXILIAR_RESULTS_ITERATOR_HPP + +#include +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +class results_iterator +{ + const execution_state_impl* self_{}; + std::size_t index_{}; + +public: + using value_type = resultset; + using reference = resultset_view; + using pointer = resultset_view; + using difference_type = std::ptrdiff_t; + using iterator_category = std::random_access_iterator_tag; + + results_iterator() = default; + results_iterator(const execution_state_impl* self, std::size_t index) noexcept + : self_(self), index_(index) + { + } + + results_iterator& operator++() noexcept + { + ++index_; + return *this; + } + results_iterator operator++(int) noexcept + { + auto res = *this; + ++(*this); + return res; + } + results_iterator& operator--() noexcept + { + --index_; + return *this; + } + results_iterator operator--(int) noexcept + { + auto res = *this; + --(*this); + return res; + } + results_iterator& operator+=(std::ptrdiff_t n) noexcept + { + index_ += n; + return *this; + } + results_iterator& operator-=(std::ptrdiff_t n) noexcept + { + index_ -= n; + return *this; + } + results_iterator operator+(std::ptrdiff_t n) const noexcept + { + return results_iterator(self_, index_ + n); + } + results_iterator operator-(std::ptrdiff_t n) const noexcept { return *this + (-n); } + std::ptrdiff_t operator-(results_iterator rhs) const noexcept { return index_ - rhs.index_; } + + pointer operator->() const noexcept { return **this; } + reference operator*() const noexcept { return (*this)[0]; } + reference operator[](std::ptrdiff_t i) const noexcept + { + return resultset_view_access::construct(*self_, index_ + i); + } + + bool operator==(results_iterator rhs) const noexcept { return index_ == rhs.index_; } + bool operator!=(results_iterator rhs) const noexcept { return !(*this == rhs); } + bool operator<(results_iterator rhs) const noexcept { return index_ < rhs.index_; } + bool operator<=(results_iterator rhs) const noexcept { return index_ <= rhs.index_; } + bool operator>(results_iterator rhs) const noexcept { return index_ > rhs.index_; } + bool operator>=(results_iterator rhs) const noexcept { return index_ >= rhs.index_; } + + std::size_t index() const noexcept { return index_; } + const execution_state_impl* obj() const noexcept { return self_; } +}; + +inline results_iterator operator+(std::ptrdiff_t n, results_iterator it) noexcept { return it + n; } + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/detail/auxiliar/row_base.hpp b/include/boost/mysql/detail/auxiliar/row_base.hpp deleted file mode 100644 index e2335eab..00000000 --- a/include/boost/mysql/detail/auxiliar/row_base.hpp +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_AUXILIAR_ROW_BASE_HPP -#define BOOST_MYSQL_DETAIL_AUXILIAR_ROW_BASE_HPP - -#include - -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -// Base class providing implementation helpers for row and rows. -// Models a field_view vector with strings pointing into a -// single character buffer. -class row_base -{ -public: - row_base() = default; - inline row_base(const field_view* fields, std::size_t size); - inline row_base(const row_base&); - row_base(row_base&&) = default; - inline row_base& operator=(const row_base&); - row_base& operator=(row_base&&) = default; - ~row_base() = default; - - inline void assign(const field_view* fields, std::size_t size); - inline void copy_strings(); - inline void clear() noexcept - { - fields_.clear(); - string_buffer_.clear(); - } - -protected: - std::vector fields_; - -private: - std::vector string_buffer_; -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -#include - -#endif diff --git a/include/boost/mysql/detail/auxiliar/row_impl.hpp b/include/boost/mysql/detail/auxiliar/row_impl.hpp new file mode 100644 index 00000000..30196a0a --- /dev/null +++ b/include/boost/mysql/detail/auxiliar/row_impl.hpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_AUXILIAR_ROW_IMPL_HPP +#define BOOST_MYSQL_DETAIL_AUXILIAR_ROW_IMPL_HPP + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Adds num_fields default-constructed fields to the vector, return pointer to the first +// allocated value. Used to allocate fields before deserialization +inline field_view* add_fields(std::vector& storage, std::size_t num_fields) +{ + std::size_t old_size = storage.size(); + storage.resize(old_size + num_fields); + return storage.data() + old_size; +} + +// A field_view vector with strings pointing into a +// single character buffer. Used to implement owning row types +class row_impl +{ +public: + row_impl() = default; + inline row_impl(const row_impl&); + row_impl(row_impl&&) = default; + inline row_impl& operator=(const row_impl&); + row_impl& operator=(row_impl&&) = default; + ~row_impl() = default; + + // Copies the given span into *this + inline row_impl(const field_view* fields, std::size_t size); + + // Copies the given span into *this, used by row/rows in assignment from view + inline void assign(const field_view* fields, std::size_t size); + + // Adds new default constructed fields to provide storage to deserialization + field_view* add_fields(std::size_t num_fields) + { + return ::boost::mysql::detail::add_fields(fields_, num_fields); + } + + // Saves strings in the [first, first+num_fields) range into the string buffer, used by execute + inline void copy_strings_as_offsets(std::size_t first, std::size_t num_fields); + + // Restores any offsets into string views, used by execute + inline void offsets_to_string_views() + { + for (auto& f : fields_) + field_view_access::offset_to_string_view(f, string_buffer_.data()); + } + + const std::vector& fields() const noexcept { return fields_; } + + inline void clear() noexcept + { + fields_.clear(); + string_buffer_.clear(); + } + +private: + inline void copy_strings(); + + std::vector fields_; + std::vector string_buffer_; +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#include + +#endif diff --git a/include/boost/mysql/detail/channel/channel.hpp b/include/boost/mysql/detail/channel/channel.hpp index 9444818a..cb3aa4ce 100644 --- a/include/boost/mysql/detail/channel/channel.hpp +++ b/include/boost/mysql/detail/channel/channel.hpp @@ -50,7 +50,6 @@ public: channel_base(std::size_t read_buffer_size) : reader_(read_buffer_size) {} // Reading - const std::uint8_t* buffer_first() const noexcept { return reader_.buffer_first(); } bool has_read_messages() const noexcept { return reader_.has_message(); } boost::asio::const_buffer next_read_message(std::uint8_t& seqnum, error_code& err) noexcept { @@ -107,28 +106,25 @@ public: executor_type get_executor() { return stream_.get_executor(); } // Reading - void read_some(error_code& code, bool keep_messages = false) - { - return reader_.read_some(stream_, code, keep_messages); - } + void read_some(error_code& code) { return reader_.read_some(stream_, code); } template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) - async_read_some(CompletionToken&& token, bool keep_messages = false) + async_read_some(CompletionToken&& token) { - return reader_.async_read_some(stream_, std::forward(token), keep_messages); + return reader_.async_read_some(stream_, std::forward(token)); } - boost::asio::const_buffer read_one(std::uint8_t& seqnum, error_code& ec, bool keep_messages = false) + boost::asio::const_buffer read_one(std::uint8_t& seqnum, error_code& ec) { - return reader_.read_one(stream_, seqnum, ec, keep_messages); + return reader_.read_one(stream_, seqnum, ec); } template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code, ::boost::asio::const_buffer)) - async_read_one(std::uint8_t& seqnum, CompletionToken&& token, bool keep_messages = false) + async_read_one(std::uint8_t& seqnum, CompletionToken&& token) { - return reader_.async_read_one(stream_, seqnum, std::forward(token), keep_messages); + return reader_.async_read_one(stream_, seqnum, std::forward(token)); } // Writing diff --git a/include/boost/mysql/detail/channel/impl/message_reader.hpp b/include/boost/mysql/detail/channel/impl/message_reader.hpp index 0f547377..abe48f0e 100644 --- a/include/boost/mysql/detail/channel/impl/message_reader.hpp +++ b/include/boost/mysql/detail/channel/impl/message_reader.hpp @@ -39,19 +39,14 @@ boost::asio::const_buffer boost::mysql::detail::message_reader::get_next_message } template -void boost::mysql::detail::message_reader::read_some( - Stream& stream, - error_code& ec, - bool keep_messages -) +void boost::mysql::detail::message_reader::read_some(Stream& stream, error_code& ec) { // If we already have a message, complete immediately if (has_message()) return; - // Remove processed messages if we can - if (!keep_messages) - buffer_.remove_reserved(); + // Remove processed messages + buffer_.remove_reserved(); while (!has_message()) { @@ -74,13 +69,9 @@ template struct boost::mysql::detail::message_reader::read_some_op : boost::asio::coroutine { message_reader& reader_; - bool keep_messages_; Stream& stream_; - read_some_op(message_reader& reader, bool keep_messages, Stream& stream) noexcept - : reader_(reader), keep_messages_(keep_messages), stream_(stream) - { - } + read_some_op(message_reader& reader, Stream& stream) noexcept : reader_(reader), stream_(stream) {} template void operator()(Self& self, error_code ec = {}, std::size_t bytes_read = 0) @@ -103,9 +94,8 @@ struct boost::mysql::detail::message_reader::read_some_op : boost::asio::corouti BOOST_ASIO_CORO_YIELD break; } - // Remove processed messages if we can - if (!keep_messages_) - reader_.buffer_.remove_reserved(); + // Remove processed messages + reader_.buffer_.remove_reserved(); while (!reader_.has_message()) { @@ -114,10 +104,7 @@ struct boost::mysql::detail::message_reader::read_some_op : boost::asio::corouti reader_.maybe_resize_buffer(); // Actually read bytes - BOOST_ASIO_CORO_YIELD stream_.async_read_some( - reader_.buffer_.free_area(), - std::move(self) - ); + BOOST_ASIO_CORO_YIELD stream_.async_read_some(reader_.buffer_.free_area(), std::move(self)); valgrind_make_mem_defined(reader_.buffer_.free_first(), bytes_read); // Process them @@ -129,18 +116,12 @@ struct boost::mysql::detail::message_reader::read_some_op : boost::asio::corouti } }; -template < - class Stream, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> +template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(::boost::mysql::error_code)) -boost::mysql::detail::message_reader::async_read_some( - Stream& stream, - CompletionToken&& token, - bool keep_messages -) +boost::mysql::detail::message_reader::async_read_some(Stream& stream, CompletionToken&& token) { return boost::asio::async_compose( - read_some_op{*this, keep_messages, stream}, + read_some_op{*this, stream}, token, stream ); @@ -150,11 +131,10 @@ template boost::asio::const_buffer boost::mysql::detail::message_reader::read_one( Stream& stream, std::uint8_t& seqnum, - error_code& ec, - bool keep_messages + error_code& ec ) { - read_some(stream, ec, keep_messages); + read_some(stream, ec); if (ec) return {}; else @@ -165,12 +145,11 @@ template struct boost::mysql::detail::message_reader::read_one_op : boost::asio::coroutine { message_reader& reader_; - bool keep_messages_; Stream& stream_; std::uint8_t& seqnum_; - read_one_op(message_reader& reader, bool keep_messages, Stream& stream, std::uint8_t& seqnum) - : reader_(reader), keep_messages_(keep_messages), stream_(stream), seqnum_(seqnum) + read_one_op(message_reader& reader, Stream& stream, std::uint8_t& seqnum) + : reader_(reader), stream_(stream), seqnum_(seqnum) { } @@ -188,7 +167,7 @@ struct boost::mysql::detail::message_reader::read_one_op : boost::asio::coroutin boost::asio::const_buffer b; BOOST_ASIO_CORO_REENTER(*this) { - BOOST_ASIO_CORO_YIELD reader_.async_read_some(stream_, std::move(self), keep_messages_); + BOOST_ASIO_CORO_YIELD reader_.async_read_some(stream_, std::move(self)); b = reader_.get_next_message(seqnum_, code); self.complete(code, b); } @@ -206,12 +185,11 @@ BOOST_ASIO_INITFN_AUTO_RESULT_TYPE( boost::mysql::detail::message_reader::async_read_one( Stream& stream, std::uint8_t& seqnum, - CompletionToken&& token, - bool keep_messages + CompletionToken&& token ) { return boost::asio::async_compose( - read_one_op{*this, keep_messages, stream, seqnum}, + read_one_op{*this, stream, seqnum}, token, stream ); diff --git a/include/boost/mysql/detail/channel/message_reader.hpp b/include/boost/mysql/detail/channel/message_reader.hpp index 64105f1e..2dfe1e2d 100644 --- a/include/boost/mysql/detail/channel/message_reader.hpp +++ b/include/boost/mysql/detail/channel/message_reader.hpp @@ -33,38 +33,31 @@ public: } bool has_message() const noexcept { return result_.has_message; } - const std::uint8_t* buffer_first() const noexcept { return buffer_.first(); } inline boost::asio::const_buffer get_next_message(std::uint8_t& seqnum, error_code& ec) noexcept; // Reads some messages from stream, until there is at least one // or an error happens. On success, has_message() returns true // and get_next_message() returns the parsed message. - // May relocate the buffer, modifying buffer_first(). If !keep_messages, - // the reserved area bytes will be removed before the actual read; otherwise, - // they won't be touched (but may be reallocated). + // May relocate the buffer, modifying buffer_first(). + // The reserved area bytes will be removed before the actual read. template - void read_some(Stream& stream, error_code& ec, bool keep_messages = false); + void read_some(Stream& stream, error_code& ec); template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) - async_read_some(Stream& stream, CompletionToken&& token, bool keep_messages = false); + async_read_some(Stream& stream, CompletionToken&& token); // Equivalent to read_some + get_next_message template - boost::asio::const_buffer read_one( - Stream& stream, - std::uint8_t& seqnum, - error_code& ec, - bool keep_messages = false - ); + boost::asio::const_buffer read_one(Stream& stream, std::uint8_t& seqnum, error_code& ec); template < class Stream, BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, boost::asio::const_buffer)) CompletionToken> BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code, boost::asio::const_buffer)) - async_read_one(Stream& stream, std::uint8_t& seqnum, CompletionToken&& token, bool keep_messages = false); + async_read_one(Stream& stream, std::uint8_t& seqnum, CompletionToken&& token); // Exposed for the sake of testing read_buffer& buffer() noexcept { return buffer_; } diff --git a/include/boost/mysql/detail/network_algorithms/start_query.hpp b/include/boost/mysql/detail/network_algorithms/execute.hpp similarity index 63% rename from include/boost/mysql/detail/network_algorithms/start_query.hpp rename to include/boost/mysql/detail/network_algorithms/execute.hpp index fbde86f3..4e834330 100644 --- a/include/boost/mysql/detail/network_algorithms/start_query.hpp +++ b/include/boost/mysql/detail/network_algorithms/execute.hpp @@ -5,15 +5,15 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_QUERY_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_QUERY_HPP +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_HPP #include #include -#include -#include +#include #include +#include #include @@ -21,21 +21,24 @@ namespace boost { namespace mysql { namespace detail { +// The caller function must serialize the execution request into channel's buffer +// before calling these + template -void start_query( +void execute( channel& channel, - string_view query, - execution_state& output, + resultset_encoding enc, + results& output, error_code& err, diagnostics& diag ); template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) -async_start_query( +async_execute( channel& chan, - string_view query, - execution_state& output, + resultset_encoding enc, + results& output, diagnostics& diag, CompletionToken&& token ); @@ -44,6 +47,6 @@ async_start_query( } // namespace mysql } // namespace boost -#include +#include -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_QUERY_HPP_ */ +#endif diff --git a/include/boost/mysql/detail/network_algorithms/execute_statement.hpp b/include/boost/mysql/detail/network_algorithms/execute_statement.hpp deleted file mode 100644 index 13e673db..00000000 --- a/include/boost/mysql/detail/network_algorithms/execute_statement.hpp +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_STATEMENT_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_STATEMENT_HPP - -#include -#include -#include -#include - -#include -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -template -void execute_statement( - channel& channel, - const statement& stmt, - const FieldLikeTuple& params, - results& output, - error_code& err, - diagnostics& diag -); - -template < - class Stream, - BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) -async_execute_statement( - channel& chan, - const statement& stmt, - FieldLikeTuple&& params, - results& output, - diagnostics& diag, - CompletionToken&& token -); - -} // namespace detail -} // namespace mysql -} // namespace boost - -#include - -#endif diff --git a/include/boost/mysql/detail/network_algorithms/helpers.hpp b/include/boost/mysql/detail/network_algorithms/helpers.hpp new file mode 100644 index 00000000..468bc687 --- /dev/null +++ b/include/boost/mysql/detail/network_algorithms/helpers.hpp @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_HELPERS_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_HELPERS_HPP + +#include +#include + +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +inline void process_available_rows( + channel_base& channel, + execution_state_impl& st, + error_code& err, + diagnostics& diag +) +{ + // Process all read messages until they run out, an error happens + // or an EOF is received + st.on_row_batch_start(); + while (channel.has_read_messages() && st.should_read_rows()) + { + // Get the row message + auto message = channel.next_read_message(st.sequence_number(), err); + if (err) + return; + + // Deserialize the row + deserialize_row(message, channel.current_capabilities(), channel.flavor(), st, err, diag); + if (err) + return; + } + st.on_row_batch_finish(); +} + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/detail/network_algorithms/start_statement_execution.hpp b/include/boost/mysql/detail/network_algorithms/high_level_execution.hpp similarity index 53% rename from include/boost/mysql/detail/network_algorithms/start_statement_execution.hpp rename to include/boost/mysql/detail/network_algorithms/high_level_execution.hpp index 775911ca..436846e3 100644 --- a/include/boost/mysql/detail/network_algorithms/start_statement_execution.hpp +++ b/include/boost/mysql/detail/network_algorithms/high_level_execution.hpp @@ -5,12 +5,12 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_STATEMENT_EXECUTION_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_STATEMENT_EXECUTION_HPP +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_HIGH_LEVEL_EXECUTION_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_HIGH_LEVEL_EXECUTION_HPP #include #include -#include +#include #include #include @@ -22,6 +22,62 @@ namespace boost { namespace mysql { namespace detail { +template +void query(channel& channel, string_view query, results& output, error_code& err, diagnostics& diag); + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) +async_query( + channel& chan, + string_view query, + results& output, + diagnostics& diag, + CompletionToken&& token +); + +template +void start_query( + channel& channel, + string_view query, + execution_state& output, + error_code& err, + diagnostics& diag +); + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) +async_start_query( + channel& chan, + string_view query, + execution_state& output, + diagnostics& diag, + CompletionToken&& token +); + +template +void execute_statement( + channel& channel, + const statement& stmt, + const FieldLikeTuple& params, + results& output, + error_code& err, + diagnostics& diag +); + +template < + class Stream, + BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) +async_execute_statement( + channel& chan, + const statement& stmt, + FieldLikeTuple&& params, + results& output, + diagnostics& diag, + CompletionToken&& token +); + template void start_statement_execution( channel& channel, @@ -76,6 +132,6 @@ async_start_statement_execution( } // namespace mysql } // namespace boost -#include +#include -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_EXECUTE_STATEMENT_HPP_ */ +#endif diff --git a/include/boost/mysql/detail/network_algorithms/impl/execute.hpp b/include/boost/mysql/detail/network_algorithms/impl/execute.hpp new file mode 100644 index 00000000..2556857b --- /dev/null +++ b/include/boost/mysql/detail/network_algorithms/impl/execute.hpp @@ -0,0 +1,159 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_HPP + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +template +struct execute_op : boost::asio::coroutine +{ + channel& chan_; + resultset_encoding enc_; + execution_state_impl& st_; + diagnostics& diag_; + + execute_op( + channel& chan, + resultset_encoding enc, + execution_state_impl& st, + diagnostics& diag + ) noexcept + : chan_(chan), enc_(enc), st_(st), diag_(diag) + { + } + + template + void operator()(Self& self, error_code err = {}) + { + // Error checking + if (err) + { + self.complete(err); + return; + } + + // Normal path + BOOST_ASIO_CORO_REENTER(*this) + { + // Setup + diag_.clear(); + st_.reset(enc_, nullptr); // rows owned by st + + // Send the execution request (already serialized at this point) + BOOST_ASIO_CORO_YIELD chan_ + .async_write(chan_.shared_buffer(), st_.sequence_number(), std::move(self)); + + while (!st_.complete()) + { + BOOST_ASIO_CORO_YIELD async_read_resultset_head(chan_, st_, diag_, std::move(self)); + + while (st_.should_read_rows()) + { + // Ensure we have messages to be read + if (!chan_.has_read_messages()) + { + BOOST_ASIO_CORO_YIELD chan_.async_read_some(std::move(self)); + } + + // Process read messages + process_available_rows(chan_, st_, err, diag_); + if (err) + { + self.complete(err); + BOOST_ASIO_CORO_YIELD break; + } + } + } + + self.complete(error_code()); + } + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +template +void boost::mysql::detail::execute( + channel& channel, + resultset_encoding enc, + results& result, + error_code& err, + diagnostics& diag +) +{ + // Setup + diag.clear(); + auto& st = results_access::get_impl(result); + st.reset(enc, nullptr); + + // Send the execution request (already serialized at this point) + channel.write(channel.shared_buffer(), st.sequence_number(), err); + if (err) + return; + + while (!st.complete()) + { + if (st.should_read_head()) + { + read_resultset_head(channel, st, err, diag); + if (err) + return; + } + + while (st.should_read_rows()) + { + // Ensure we have messages to be read + if (!channel.has_read_messages()) + { + channel.read_some(err); + if (err) + return; + } + + // Process read messages + process_available_rows(channel, st, err, diag); + if (err) + return; + } + } +} + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_execute( + channel& chan, + resultset_encoding enc, + results& result, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_compose( + execute_op(chan, enc, results_access::get_impl(result), diag), + token, + chan + ); +} + +#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_QUERY_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/execute_statement.hpp b/include/boost/mysql/detail/network_algorithms/impl/execute_statement.hpp deleted file mode 100644 index 26890949..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/execute_statement.hpp +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_STATEMENT_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_STATEMENT_HPP - -#pragma once - -#include -#include - -#include -#include -#include -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -template -struct execute_statement_op : boost::asio::coroutine -{ - channel& chan_; - diagnostics& diag_; - statement stmt_; - FieldLikeTuple params_; - results& output_; - - // We need a deduced context to enable perfect forwarding - template - execute_statement_op( - channel& chan, - diagnostics& diag, - const statement& stmt, - DeducedTuple&& params, - results& output - ) noexcept - : chan_(chan), diag_(diag), stmt_(stmt), params_(std::forward(params)), output_(output) - { - } - - template - void operator()(Self& self, error_code err = {}) - { - // Error checking - if (err) - { - self.complete(err); - return; - } - - // Normal path - BOOST_ASIO_CORO_REENTER(*this) - { - BOOST_ASIO_CORO_YIELD - async_start_statement_execution( - chan_, - stmt_, - std::move(params_), - results_access::get_state(output_), - diag_, - std::move(self) - ); - - BOOST_ASIO_CORO_YIELD async_read_all_rows( - chan_, - results_access::get_state(output_), - results_access::get_rows(output_), - diag_, - std::move(self) - ); - - self.complete(error_code()); - } - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -template -void boost::mysql::detail::execute_statement( - channel& channel, - const statement& stmt, - const FieldLikeTuple& params, - results& output, - error_code& err, - diagnostics& diag -) -{ - start_statement_execution(channel, stmt, params, results_access::get_state(output), err, diag); - if (err) - return; - - read_all_rows(channel, results_access::get_state(output), results_access::get_rows(output), err, diag); -} - -template < - class Stream, - BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_execute_statement( - channel& chan, - const statement& stmt, - FieldLikeTuple&& params, - results& output, - diagnostics& diag, - CompletionToken&& token -) -{ - using decayed_tuple = typename std::decay::type; - return boost::asio::async_compose( - execute_statement_op( - chan, - diag, - stmt, - std::forward(params), - output - ), - token, - chan - ); -} - -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_QUERY_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/handshake.hpp b/include/boost/mysql/detail/network_algorithms/impl/handshake.hpp index 70c6c7f8..e1fe43bb 100644 --- a/include/boost/mysql/detail/network_algorithms/impl/handshake.hpp +++ b/include/boost/mysql/detail/network_algorithms/impl/handshake.hpp @@ -81,6 +81,7 @@ inline error_code process_capabilities( capabilities server_caps(handshake.capability_falgs); capabilities required_caps = mandatory_capabilities | conditional_capability(!params.database().empty(), CLIENT_CONNECT_WITH_DB) | + conditional_capability(params.multi_queries(), CLIENT_MULTI_STATEMENTS) | conditional_capability( ssl == ssl_mode::require && is_ssl_stream, CLIENT_SSL diff --git a/include/boost/mysql/detail/network_algorithms/impl/high_level_execution.hpp b/include/boost/mysql/detail/network_algorithms/impl/high_level_execution.hpp new file mode 100644 index 00000000..79a50720 --- /dev/null +++ b/include/boost/mysql/detail/network_algorithms/impl/high_level_execution.hpp @@ -0,0 +1,432 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_HIGH_LEVEL_EXECUTION_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_HIGH_LEVEL_EXECUTION_HPP + +#pragma once +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Helpers +inline void serialize_query_exec_req(channel_base& chan, string_view query) +{ + com_query_packet request{string_eof(query)}; + serialize_message(request, chan.current_capabilities(), chan.shared_buffer()); +} + +template +void serialize_stmt_exec_req( + channel_base& chan, + const statement& stmt, + FieldViewFwdIterator first, + FieldViewFwdIterator last +) +{ + com_stmt_execute_packet request{ + stmt.id(), + std::uint8_t(0), // flags + std::uint32_t(1), // iteration count + std::uint8_t(1), // new params flag: set + first, + last, + }; + serialize_message(request, chan.current_capabilities(), chan.shared_buffer()); +} + +template +std::array tuple_to_array_impl(const std::tuple& t, boost::mp11::index_sequence) noexcept +{ + return std::array{{field_view(std::get(t))...}}; +} + +template +std::array tuple_to_array(const std::tuple& t) noexcept +{ + return tuple_to_array_impl(t, boost::mp11::make_index_sequence()); +} + +template +void serialize_stmt_exec_req(channel_base& chan, const statement& stmt, const FieldLikeTuple& params) +{ + auto arr = tuple_to_array(params); + serialize_stmt_exec_req(chan, stmt, arr.begin(), arr.end()); +} + +inline error_code check_num_params(const statement& stmt, std::size_t param_count) +{ + return param_count == stmt.num_params() ? error_code() : make_error_code(client_errc::wrong_num_params); +} + +template +error_code check_num_params_it( + const statement& stmt, + FieldViewFwdIterator params_first, + FieldViewFwdIterator params_last +) +{ + return check_num_params(stmt, std::distance(params_first, params_last)); +} + +template +error_code check_num_params_tuple(const statement& stmt, const FieldLikeTuple&) +{ + return check_num_params(stmt, std::tuple_size::value); +} + +template +void fast_fail(channel& chan, Handler&& handler, error_code ec) +{ + boost::asio::post(boost::asio::bind_executor( + boost::asio::get_associated_executor(handler, chan.get_executor()), + std::bind(std::forward(handler), ec) + )); +} + +// Async initiations +struct initiate_query +{ + template + void operator()( + Handler&& handler, + std::reference_wrapper> chan, + string_view query, + results& result, + diagnostics& diag + ) + { + serialize_query_exec_req(chan, query); + async_execute(chan.get(), resultset_encoding::text, result, diag, std::forward(handler)); + } +}; + +struct initiate_start_query +{ + template + void operator()( + Handler&& handler, + std::reference_wrapper> chan, + string_view query, + execution_state& st, + diagnostics& diag + ) + { + serialize_query_exec_req(chan, query); + async_start_execution(chan.get(), resultset_encoding::text, st, diag, std::forward(handler)); + } +}; + +struct initiate_execute_statement +{ + template + void operator()( + Handler&& handler, + std::reference_wrapper> chan, + const statement& stmt, + const FieldLikeTuple& params, + results& result, + diagnostics& diag + ) + { + auto ec = check_num_params_tuple(stmt, params); + if (ec) + { + diag.clear(); + fast_fail(chan.get(), std::forward(handler), ec); + } + else + { + serialize_stmt_exec_req(chan, stmt, params); + async_execute( + chan.get(), + resultset_encoding::binary, + result, + diag, + std::forward(handler) + ); + } + } +}; + +struct initiate_start_statement_execution +{ + template + void operator()( + Handler&& handler, + std::reference_wrapper> chan, + const statement& stmt, + const FieldLikeTuple& params, + execution_state& st, + diagnostics& diag + ) + { + auto ec = check_num_params_tuple(stmt, params); + if (ec) + { + diag.clear(); + fast_fail(chan.get(), std::forward(handler), ec); + } + else + { + serialize_stmt_exec_req(chan, stmt, params); + async_start_execution( + chan.get(), + resultset_encoding::binary, + st, + diag, + std::forward(handler) + ); + } + } + + template + void operator()( + Handler&& handler, + std::reference_wrapper> chan, + const statement& stmt, + FwdIt first, + FwdIt last, + execution_state& st, + diagnostics& diag + ) + { + auto ec = check_num_params_it(stmt, first, last); + if (ec) + { + diag.clear(); + fast_fail(chan.get(), std::forward(handler), ec); + } + else + { + serialize_stmt_exec_req(chan, stmt, first, last); + async_start_execution( + chan.get(), + resultset_encoding::binary, + st, + diag, + std::forward(handler) + ); + } + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +template +void boost::mysql::detail::query( + channel& channel, + string_view query, + results& result, + error_code& err, + diagnostics& diag +) +{ + serialize_query_exec_req(channel, query); + execute(channel, resultset_encoding::text, result, err, diag); +} + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_query( + channel& chan, + string_view query, + results& result, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_initiate( + initiate_query(), + token, + std::ref(chan), + query, + std::ref(result), + std::ref(diag) + ); +} + +template +void boost::mysql::detail::start_query( + channel& channel, + string_view query, + execution_state& st, + error_code& err, + diagnostics& diag +) +{ + serialize_query_exec_req(channel, query); + start_execution(channel, resultset_encoding::text, st, err, diag); +} + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_start_query( + channel& chan, + string_view query, + execution_state& st, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_initiate( + initiate_start_query(), + token, + std::ref(chan), + query, + std::ref(st), + std::ref(diag) + ); +} + +template +void boost::mysql::detail::execute_statement( + channel& channel, + const statement& stmt, + const FieldLikeTuple& params, + results& result, + error_code& err, + diagnostics& diag +) +{ + err = check_num_params_tuple(stmt, params); + if (err) + return; + + serialize_stmt_exec_req(channel, stmt, params); + execute(channel, resultset_encoding::binary, result, err, diag); +} + +template < + class Stream, + BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_execute_statement( + channel& chan, + const statement& stmt, + FieldLikeTuple&& params, + results& result, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_initiate( + initiate_execute_statement(), + token, + std::ref(chan), + stmt, + std::forward(params), + std::ref(result), + std::ref(diag) + ); +} + +template +void boost::mysql::detail::start_statement_execution( + channel& channel, + const statement& stmt, + const FieldLikeTuple& params, + execution_state& st, + error_code& err, + diagnostics& diag +) +{ + err = check_num_params_tuple(stmt, params); + if (err) + return; + + serialize_stmt_exec_req(channel, stmt, params); + start_execution(channel, resultset_encoding::binary, st, err, diag); +} + +template < + class Stream, + BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_start_statement_execution( + channel& chan, + const statement& stmt, + FieldLikeTuple&& params, + execution_state& output, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_initiate( + initiate_start_statement_execution(), + token, + std::ref(chan), + stmt, + std::forward(params), + std::ref(output), + std::ref(diag) + ); +} + +template +void boost::mysql::detail::start_statement_execution( + channel& chan, + const statement& stmt, + FieldViewFwdIterator params_first, + FieldViewFwdIterator params_last, + execution_state& st, + error_code& err, + diagnostics& diag +) +{ + err = check_num_params_it(stmt, params_first, params_last); + if (err) + return; + + serialize_stmt_exec_req(chan, stmt, params_first, params_last); + start_execution(chan, resultset_encoding::binary, st, err, diag); +} + +template < + class Stream, + BOOST_MYSQL_FIELD_VIEW_FORWARD_ITERATOR FieldViewFwdIterator, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_start_statement_execution( + channel& chan, + const statement& stmt, + FieldViewFwdIterator params_first, + FieldViewFwdIterator params_last, + execution_state& st, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_initiate( + initiate_start_statement_execution(), + token, + std::ref(chan), + stmt, + params_first, + params_last, + std::ref(st), + std::ref(diag) + ); +} + +#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_QUERY_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/query.hpp b/include/boost/mysql/detail/network_algorithms/impl/query.hpp deleted file mode 100644 index 765b473d..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/query.hpp +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_QUERY_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_QUERY_HPP - -#pragma once - -#include -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -template -struct query_op : boost::asio::coroutine -{ - channel& chan_; - diagnostics& diag_; - string_view query_; - results& output_; - - query_op(channel& chan, diagnostics& diag, string_view q, results& output) noexcept - : chan_(chan), diag_(diag), query_(q), output_(output) - { - } - - template - void operator()(Self& self, error_code err = {}) - { - // Error checking - if (err) - { - self.complete(err); - return; - } - - // Normal path - BOOST_ASIO_CORO_REENTER(*this) - { - BOOST_ASIO_CORO_YIELD - async_start_query(chan_, query_, results_access::get_state(output_), diag_, std::move(self)); - - BOOST_ASIO_CORO_YIELD async_read_all_rows( - chan_, - results_access::get_state(output_), - results_access::get_rows(output_), - diag_, - std::move(self) - ); - - self.complete(error_code()); - } - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -template -void boost::mysql::detail::query( - channel& channel, - string_view query, - results& output, - error_code& err, - diagnostics& diag -) -{ - start_query(channel, query, results_access::get_state(output), err, diag); - if (err) - return; - - read_all_rows(channel, results_access::get_state(output), results_access::get_rows(output), err, diag); -} - -template -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_query( - channel& chan, - string_view query, - results& output, - diagnostics& diag, - CompletionToken&& token -) -{ - return boost::asio::async_compose( - query_op(chan, diag, query, output), - token, - chan - ); -} - -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_QUERY_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/quit_connection.hpp b/include/boost/mysql/detail/network_algorithms/impl/quit_connection.hpp index 4099971b..b83a4420 100644 --- a/include/boost/mysql/detail/network_algorithms/impl/quit_connection.hpp +++ b/include/boost/mysql/detail/network_algorithms/impl/quit_connection.hpp @@ -53,7 +53,7 @@ struct quit_connection_op : boost::asio::coroutine } // SSL shutdown error ignored, as MySQL doesn't always gracefully - // close SSL connections. TODO: was this because of a missing if? + // close SSL connections. if (chan_.stream().ssl_active()) { BOOST_ASIO_CORO_YIELD chan_.stream().async_shutdown(std::move(self)); diff --git a/include/boost/mysql/detail/network_algorithms/impl/read_all_rows.hpp b/include/boost/mysql/detail/network_algorithms/impl/read_all_rows.hpp deleted file mode 100644 index 361a3c80..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/read_all_rows.hpp +++ /dev/null @@ -1,195 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_READ_ALL_ROWS_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_READ_ALL_ROWS_HPP - -#pragma once - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -template -inline void process_all_rows( - channel& channel, - execution_state& st, - rows& output, - error_code& err, - diagnostics& diag -) -{ - // Process all read messages until they run out, an error happens - // or an EOF is received - while (channel.has_read_messages()) - { - // Get the row message - auto message = channel.next_read_message(execution_state_access::get_sequence_number(st), err); - if (err) - return; - - // Deserialize the row. Values are stored in a vector owned by the channel - deserialize_row( - message, - channel.current_capabilities(), - channel.flavor(), - channel.buffer_first(), - st, - channel.shared_fields(), - err, - diag - ); - if (err) - return; - - // If we received an EOF, we're done - if (st.complete()) - { - offsets_to_string_views(channel.shared_fields(), channel.buffer_first()); - output = rows_view_access::construct( - channel.shared_fields().data(), - channel.shared_fields().size(), - st.meta().size() - ); - break; - } - } -} - -template -struct read_all_rows_op : boost::asio::coroutine -{ - channel& chan_; - diagnostics& diag_; - execution_state& st_; - rows& output_; - - read_all_rows_op(channel& chan, diagnostics& diag, execution_state& st, rows& output) noexcept - : chan_(chan), diag_(diag), st_(st), output_(output) - { - } - - template - void operator()(Self& self, error_code err = {}) - { - // Error checking - if (err) - { - self.complete(err); - return; - } - - // Normal path - BOOST_ASIO_CORO_REENTER(*this) - { - diag_.clear(); - rows_access::clear(output_); - - // If the op is already complete, we don't need to read anything - if (st_.complete()) - { - BOOST_ASIO_CORO_YIELD boost::asio::post(std::move(self)); - self.complete(error_code()); - BOOST_ASIO_CORO_YIELD break; - } - - // Clear anything from previous runs - chan_.shared_fields().clear(); - - // Read at least one message - while (!st_.complete()) - { - // Actually read - BOOST_ASIO_CORO_YIELD chan_.async_read_some(std::move(self), true); - - // Process messages - process_all_rows(chan_, st_, output_, err, diag_); - if (err) - { - self.complete(err); - BOOST_ASIO_CORO_YIELD break; - } - } - - // Done - self.complete(error_code()); - } - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -template -void boost::mysql::detail::read_all_rows( - channel& channel, - execution_state& st, - rows& output, - error_code& err, - diagnostics& diag -) -{ - diag.clear(); - rows_access::clear(output); - - // If the op is already complete, we don't need to read anything - if (st.complete()) - { - err = error_code(); - return; - } - - // Clear anything from previous runs - channel.shared_fields().clear(); - - while (!st.complete()) - { - // Read from the stream until there is at least one message - channel.read_some(err, true); - if (err) - return; - - // Process read messages - process_all_rows(channel, st, output, err, diag); - if (err) - return; - } -} - -template -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_read_all_rows( - channel& channel, - execution_state& st, - rows& output, - diagnostics& diag, - CompletionToken&& token -) -{ - return boost::asio::async_compose( - read_all_rows_op(channel, diag, st, output), - token, - channel - ); -} - -#endif /* INCLUDE_MYSQL_IMPL_NETWORK_ALGORITHMS_READ_TEXT_ROW_IPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/read_resultset_head.hpp b/include/boost/mysql/detail/network_algorithms/impl/read_resultset_head.hpp new file mode 100644 index 00000000..392f14bd --- /dev/null +++ b/include/boost/mysql/detail/network_algorithms/impl/read_resultset_head.hpp @@ -0,0 +1,223 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_READ_RESULTSET_HEAD_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_READ_RESULTSET_HEAD_HPP + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +class read_resultset_head_processor +{ + channel_base& chan_; + execution_state_impl& st_; + diagnostics& diag_; + +public: + read_resultset_head_processor(channel_base& chan, execution_state_impl& st, diagnostics& diag) noexcept + : chan_(chan), st_(st), diag_(diag) + { + } + + void clear_diag() noexcept { diag_.clear(); } + + error_code process_response(boost::asio::const_buffer msg) + { + error_code err; + auto + response = deserialize_execute_response(msg, chan_.current_capabilities(), chan_.flavor(), diag_); + switch (response.type) + { + case execute_response::type_t::error: err = response.data.err; break; + case execute_response::type_t::ok_packet: st_.on_head_ok_packet(response.data.ok_pack); break; + case execute_response::type_t::num_fields: st_.on_num_meta(response.data.num_fields); break; + } + return err; + } + + error_code process_field_definition() + { + // Read the field definition packet (it's cached at this point) + assert(chan_.has_read_messages()); + error_code err; + auto msg = chan_.next_read_message(st_.sequence_number(), err); + if (err) + return err; + + // Deserialize + column_definition_packet field_definition{}; + deserialization_context ctx(msg, chan_.current_capabilities()); + err = deserialize_message(ctx, field_definition); + if (err) + return err; + + // Notify the state object + st_.on_meta(field_definition, chan_.meta_mode()); + return error_code(); + } + + channel_base& get_channel() noexcept { return chan_; } + execution_state_impl& state() noexcept { return st_; } +}; + +template +struct read_resultset_head_op : boost::asio::coroutine +{ + read_resultset_head_processor processor_; + + read_resultset_head_op(channel& chan, execution_state_impl& st, diagnostics& diag) + : processor_(chan, st, diag) + { + } + + channel& get_channel() noexcept + { + return static_cast&>(processor_.get_channel()); + } + + template + void operator()(Self& self, error_code err = {}, boost::asio::const_buffer read_message = {}) + { + // Error checking + if (err) + { + self.complete(err); + return; + } + + // Non-error path + BOOST_ASIO_CORO_REENTER(*this) + { + processor_.clear_diag(); + + // If we're not reading head, return + if (!processor_.state().should_read_head()) + { + BOOST_ASIO_CORO_YIELD boost::asio::post(std::move(self)); + self.complete(error_code()); + BOOST_ASIO_CORO_YIELD break; + } + + // Read the response + BOOST_ASIO_CORO_YIELD get_channel().async_read_one( + processor_.state().sequence_number(), + std::move(self) + ); + + // Response may be: ok_packet, err_packet, local infile request + // (not implemented), or response with fields + err = processor_.process_response(read_message); + if (err) + { + self.complete(err); + BOOST_ASIO_CORO_YIELD break; + } + + // Read all of the field definitions + while (processor_.state().should_read_meta()) + { + // Read from the stream if we need it + if (!get_channel().has_read_messages()) + { + BOOST_ASIO_CORO_YIELD get_channel().async_read_some(std::move(self)); + } + + // Process the metadata packet + err = processor_.process_field_definition(); + if (err) + { + self.complete(err); + BOOST_ASIO_CORO_YIELD break; + } + } + + // No EOF packet is expected here, as we require deprecate EOF capabilities + self.complete(error_code()); + } + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +template +void boost::mysql::detail::read_resultset_head( + channel& chan, + execution_state_impl& st, + error_code& err, + diagnostics& diag +) +{ + // Setup + read_resultset_head_processor processor(chan, st, diag); + processor.clear_diag(); + + // If we're not reading head, return + if (!st.should_read_head()) + return; + + // Read the response + auto msg = chan.read_one(st.sequence_number(), err); + if (err) + return; + + // Response may be: ok_packet, err_packet, local infile request + // (not implemented), or response with fields + err = processor.process_response(msg); + if (err) + return; + + // Read all of the field definitions (zero if empty resultset) + while (st.should_read_meta()) + { + // Read from the stream if required + if (!chan.has_read_messages()) + { + chan.read_some(err); + if (err) + return; + } + + // Process the packet + err = processor.process_field_definition(); + if (err) + return; + } +} + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_read_resultset_head( + channel& channel, + execution_state_impl& st, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_compose( + read_resultset_head_op(channel, st, diag), + token, + channel + ); +} + +#endif diff --git a/include/boost/mysql/detail/network_algorithms/impl/read_some_rows.hpp b/include/boost/mysql/detail/network_algorithms/impl/read_some_rows.hpp index 4de7beb7..250568d7 100644 --- a/include/boost/mysql/detail/network_algorithms/impl/read_some_rows.hpp +++ b/include/boost/mysql/detail/network_algorithms/impl/read_some_rows.hpp @@ -9,12 +9,13 @@ #define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_READ_SOME_ROWS_HPP #pragma once - #include #include #include #include +#include +#include #include #include @@ -27,64 +28,14 @@ namespace boost { namespace mysql { namespace detail { -template -inline rows_view process_some_rows( - channel& channel, - execution_state& st, - error_code& err, - diagnostics& diag -) -{ - // Process all read messages until they run out, an error happens - // or an EOF is received - std::size_t num_rows = 0; - channel.shared_fields().clear(); - while (channel.has_read_messages()) - { - // Get the row message - auto message = channel.next_read_message(execution_state_access::get_sequence_number(st), err); - if (err) - return rows_view(); - - // Deserialize the row. Values are stored in a vector owned by the channel - deserialize_row( - message, - channel.current_capabilities(), - channel.flavor(), - channel.buffer_first(), - st, - channel.shared_fields(), - err, - diag - ); - if (err) - return rows_view(); - - // There is no need to copy strings values anywhere; the returned values - // will point into the channel's internal buffer - - // If we received an EOF, we're done - if (st.complete()) - break; - ++num_rows; - } - offsets_to_string_views(channel.shared_fields(), channel.buffer_first()); - - return rows_view_access::construct( - channel.shared_fields().data(), - num_rows * st.meta().size(), - st.meta().size() - ); -} - template struct read_some_rows_op : boost::asio::coroutine { channel& chan_; diagnostics& diag_; - execution_state& st_; + execution_state_impl& st_; - read_some_rows_op(channel& chan, diagnostics& diag, execution_state& st) noexcept + read_some_rows_op(channel& chan, diagnostics& diag, execution_state_impl& st) noexcept : chan_(chan), diag_(diag), st_(st) { } @@ -100,13 +51,12 @@ struct read_some_rows_op : boost::asio::coroutine } // Normal path - rows_view output; BOOST_ASIO_CORO_REENTER(*this) { diag_.clear(); - // If the op is already complete, we don't need to read anything - if (st_.complete()) + // If we are not reading rows, return + if (!st_.should_read_rows()) { BOOST_ASIO_CORO_YIELD boost::asio::post(std::move(self)); self.complete(error_code(), rows_view()); @@ -117,9 +67,14 @@ struct read_some_rows_op : boost::asio::coroutine BOOST_ASIO_CORO_YIELD chan_.async_read_some(std::move(self)); // Process messages - output = process_some_rows(chan_, st_, err, diag_); + process_available_rows(chan_, st_, err, diag_); + if (err) + { + self.complete(err, rows_view()); + BOOST_ASIO_CORO_YIELD break; + } - self.complete(err, output); + self.complete(error_code(), st_.get_external_rows()); } } }; @@ -136,8 +91,10 @@ boost::mysql::rows_view boost::mysql::detail::read_some_rows( diagnostics& diag ) { - // If the op is already complete, we don't need to read anything - if (st.complete()) + auto& impl = execution_state_access::get_impl(st); + + // If we are not reading rows, just return + if (!impl.should_read_rows()) { return rows_view(); } @@ -148,7 +105,11 @@ boost::mysql::rows_view boost::mysql::detail::read_some_rows( return rows_view(); // Process read messages - return process_some_rows(channel, st, err, diag); + process_available_rows(channel, impl, err, diag); + if (err) + return rows_view(); + + return impl.get_external_rows(); } template < @@ -164,7 +125,7 @@ boost::mysql::detail::async_read_some_rows( ) { return boost::asio::async_compose( - read_some_rows_op(channel, diag, st), + read_some_rows_op(channel, diag, execution_state_access::get_impl(st)), token, channel ); diff --git a/include/boost/mysql/detail/network_algorithms/impl/start_execution.hpp b/include/boost/mysql/detail/network_algorithms/impl/start_execution.hpp new file mode 100644 index 00000000..87a74631 --- /dev/null +++ b/include/boost/mysql/detail/network_algorithms/impl/start_execution.hpp @@ -0,0 +1,118 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_EXECUTION_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_EXECUTION_HPP + +#pragma once + +#include +#include +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +template +struct start_execution_op : boost::asio::coroutine +{ + channel& chan_; + resultset_encoding enc_; + execution_state_impl& st_; + diagnostics& diag_; + + start_execution_op( + channel& chan, + resultset_encoding enc, + execution_state_impl& st, + diagnostics& diag + ) + : chan_(chan), enc_(enc), st_(st), diag_(diag) + { + } + + template + void operator()(Self& self, error_code err = {}) + { + // Error checking + if (err) + { + self.complete(err); + return; + } + + // Non-error path + BOOST_ASIO_CORO_REENTER(*this) + { + // Setup + diag_.clear(); + st_.reset(enc_, &chan_.shared_fields()); + + // Send the execution request (already serialized at this point) + BOOST_ASIO_CORO_YIELD chan_ + .async_write(chan_.shared_buffer(), st_.sequence_number(), std::move(self)); + + // Read the first resultset's head + BOOST_ASIO_CORO_YIELD + async_read_resultset_head(chan_, st_, diag_, std::move(self)); + + self.complete(error_code()); + } + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +template +void boost::mysql::detail::start_execution( + channel& channel, + resultset_encoding enc, + execution_state& st, + error_code& err, + diagnostics& diag +) +{ + // Setup + auto& impl = execution_state_access::get_impl(st); + diag.clear(); + + impl.reset(enc, &channel.shared_fields()); + + // Send the execution request (already serialized at this point) + channel.write(channel.shared_buffer(), impl.sequence_number(), err); + if (err) + return; + + // Read the first resultset's head + read_resultset_head(channel, impl, err, diag); + if (err) + return; +} + +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::detail::async_start_execution( + channel& channel, + resultset_encoding enc, + execution_state& st, + diagnostics& diag, + CompletionToken&& token +) +{ + return boost::asio::async_compose( + start_execution_op(channel, enc, execution_state_access::get_impl(st), diag), + token, + channel + ); +} + +#endif diff --git a/include/boost/mysql/detail/network_algorithms/impl/start_execution_generic.hpp b/include/boost/mysql/detail/network_algorithms/impl/start_execution_generic.hpp deleted file mode 100644 index f6ab5fd4..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/start_execution_generic.hpp +++ /dev/null @@ -1,319 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_EXECUTION_GENERIC_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_EXECUTION_GENERIC_HPP - -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -class start_execution_processor -{ - channel_base& chan_; - resultset_encoding encoding_; - execution_state& st_; - diagnostics& diag_; - std::size_t num_fields_{}; - std::size_t remaining_fields_{}; - -public: - start_execution_processor( - channel_base& chan, - resultset_encoding encoding, - execution_state& st, - diagnostics& diag - ) noexcept - : chan_(chan), encoding_(encoding), st_(st), diag_(diag){}; - - void clear_diagnostics() noexcept { diag_.clear(); } - - template - void process_request(SerializeFn&& request) - { - execution_state_access::reset(st_, encoding_); - std::forward(request)(chan_.current_capabilities(), chan_.shared_buffer()); - } - - void process_response(boost::asio::const_buffer msg, error_code& err) - { - auto - response = deserialize_execute_response(msg, chan_.current_capabilities(), chan_.flavor(), diag_); - switch (response.type) - { - case execute_response::type_t::error: err = response.data.err; break; - case execute_response::type_t::ok_packet: - execution_state_access::complete(st_, response.data.ok_pack); - num_fields_ = 0; - break; - case execute_response::type_t::num_fields: - num_fields_ = response.data.num_fields; - remaining_fields_ = num_fields_; - execution_state_access::prepare_meta(st_, num_fields_); - break; - } - } - - error_code process_field_definition(boost::asio::const_buffer message) - { - column_definition_packet field_definition{}; - deserialization_context ctx(message, chan_.current_capabilities()); - auto err = deserialize_message(ctx, field_definition); - if (err) - return err; - - execution_state_access::add_meta(st_, field_definition, chan_.meta_mode()); - --remaining_fields_; - return error_code(); - } - - std::uint8_t& sequence_number() noexcept { return execution_state_access::get_sequence_number(st_); } - std::size_t num_fields() const noexcept { return num_fields_; } - bool has_remaining_fields() const noexcept { return remaining_fields_ != 0; } -}; - -template -struct start_execution_generic_op : boost::asio::coroutine -{ - channel& chan_; - SerializeFn serialize_fn_; - start_execution_processor processor_; - - template - start_execution_generic_op( - channel& chan, - DeducedSerializeFn&& fn, - const start_execution_processor& processor - ) - : chan_(chan), serialize_fn_(std::forward(fn)), processor_(processor) - { - } - - template - void operator()(Self& self, error_code err = {}, boost::asio::const_buffer read_message = {}) - { - // Error checking - if (err) - { - self.complete(err); - return; - } - - // Non-error path - BOOST_ASIO_CORO_REENTER(*this) - { - processor_.clear_diagnostics(); - - // Serialize the request - processor_.process_request(std::move(serialize_fn_)); - - // Send it - BOOST_ASIO_CORO_YIELD chan_ - .async_write(chan_.shared_buffer(), processor_.sequence_number(), std::move(self)); - - // Read the response - BOOST_ASIO_CORO_YIELD chan_.async_read_one(processor_.sequence_number(), std::move(self)); - - // Response may be: ok_packet, err_packet, local infile request - // (not implemented), or response with fields - processor_.process_response(read_message, err); - if (err) - { - self.complete(err); - BOOST_ASIO_CORO_YIELD break; - } - - // Read all of the field definitions - while (processor_.has_remaining_fields()) - { - // Read from the stream if we need it - if (!chan_.has_read_messages()) - { - BOOST_ASIO_CORO_YIELD chan_.async_read_some(std::move(self)); - } - - // Read the field definition packet - read_message = chan_.next_read_message(processor_.sequence_number(), err); - if (err) - { - self.complete(err); - BOOST_ASIO_CORO_YIELD break; - } - - // Process the message - err = processor_.process_field_definition(read_message); - if (err) - { - self.complete(err); - BOOST_ASIO_CORO_YIELD break; - } - } - - // No EOF packet is expected here, as we require deprecate EOF capabilities - self.complete(error_code()); - } - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -inline boost::mysql::detail::execute_response boost::mysql::detail::deserialize_execute_response( - boost::asio::const_buffer msg, - capabilities caps, - db_flavor flavor, - diagnostics& diag -) noexcept -{ - // Response may be: ok_packet, err_packet, local infile request (not implemented) - // If it is none of this, then the message type itself is the beginning of - // a length-encoded int containing the field count - deserialization_context ctx(msg, caps); - std::uint8_t msg_type = 0; - auto err = deserialize_message_part(ctx, msg_type); - if (err) - return err; - - if (msg_type == ok_packet_header) - { - ok_packet ok_pack; - err = deserialize_message(ctx, ok_pack); - if (err) - return err; - return ok_pack; - } - else if (msg_type == error_packet_header) - { - return process_error_packet(ctx, flavor, diag); - } - else - { - // Resultset with metadata. First packet is an int_lenenc with - // the number of field definitions to expect. Message type is part - // of this packet, so we must rewind the context - ctx.rewind(1); - int_lenenc num_fields; - err = deserialize_message(ctx, num_fields); - if (err) - return err; - - // We should have at least one field. - // The max number of fields is some value around 1024. For simplicity/extensibility, - // we accept anything less than 0xffff - if (num_fields.value == 0 || num_fields.value > 0xffffu) - { - return make_error_code(client_errc::protocol_value_error); - } - - return static_cast(num_fields.value); - } -} - -template -void boost::mysql::detail::start_execution_generic( - resultset_encoding encoding, - channel& channel, - const SerializeFn& fn, - execution_state& st, - error_code& err, - diagnostics& diag -) -{ - // Clear info - diag.clear(); - - // Serialize the request - start_execution_processor processor(channel, encoding, st, diag); - processor.process_request(fn); - - // Send it - channel.write(channel.shared_buffer(), processor.sequence_number(), err); - if (err) - return; - - // Read the response - auto read_buffer = channel.read_one(processor.sequence_number(), err); - if (err) - return; - - // Response may be: ok_packet, err_packet, local infile request (not implemented), or response - // with fields - processor.process_response(read_buffer, err); - if (err) - return; - - // Read all of the field definitions (zero if empty resultset) - while (processor.has_remaining_fields()) - { - // Read from the stream if required - if (!channel.has_read_messages()) - { - channel.read_some(err); - if (err) - return; - } - - // Read the field definition packet - read_buffer = channel.next_read_message(processor.sequence_number(), err); - if (err) - return; - - // Process the message - err = processor.process_field_definition(read_buffer); - if (err) - return; - } -} - -template < - class Stream, - BOOST_MYSQL_SERIALIZE_FN SerializeFn, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_start_execution_generic( - resultset_encoding encoding, - channel& channel, - SerializeFn&& fn, - execution_state& st, - diagnostics& diag, - CompletionToken&& token -) -{ - return boost::asio::async_compose( - start_execution_generic_op::type>( - channel, - std::forward(fn), - start_execution_processor(channel, encoding, st, diag) - ), - token, - channel - ); -} - -#endif diff --git a/include/boost/mysql/detail/network_algorithms/impl/start_query.hpp b/include/boost/mysql/detail/network_algorithms/impl/start_query.hpp deleted file mode 100644 index 1774eded..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/start_query.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_QUERY_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_QUERY_HPP - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -class query_serialize_fn -{ - string_view query_; - -public: - query_serialize_fn(string_view query) noexcept : query_(query) {} - void operator()(capabilities caps, std::vector& buffer) const - { - com_query_packet request{string_eof(query_)}; - serialize_message(request, caps, buffer); - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -template -void boost::mysql::detail::start_query( - channel& channel, - string_view query, - execution_state& output, - error_code& err, - diagnostics& diag -) -{ - start_execution_generic(resultset_encoding::text, channel, query_serialize_fn(query), output, err, diag); -} - -template -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_start_query( - channel& chan, - string_view query, - execution_state& output, - diagnostics& diag, - CompletionToken&& token -) -{ - return async_start_execution_generic( - resultset_encoding::text, - chan, - query_serialize_fn(query), - output, - diag, - std::forward(token) - ); -} - -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_QUERY_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/impl/start_statement_execution.hpp b/include/boost/mysql/detail/network_algorithms/impl/start_statement_execution.hpp deleted file mode 100644 index 2a6c5bad..00000000 --- a/include/boost/mysql/detail/network_algorithms/impl/start_statement_execution.hpp +++ /dev/null @@ -1,269 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_STATEMENT_EXECUTION_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_START_STATEMENT_EXECUTION_HPP - -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -namespace boost { -namespace mysql { -namespace detail { - -template -com_stmt_execute_packet make_stmt_execute_packet( - std::uint32_t stmt_id, - FieldViewFwdIterator params_first, - FieldViewFwdIterator params_last -) noexcept(std::is_nothrow_copy_constructible::value) -{ - return com_stmt_execute_packet{ - stmt_id, - std::uint8_t(0), // flags - std::uint32_t(1), // iteration count - std::uint8_t(1), // new params flag: set - params_first, - params_last, - }; -} - -template -std::array tuple_to_array_impl(const std::tuple& t, boost::mp11::index_sequence) noexcept -{ - return std::array{{field_view(std::get(t))...}}; -} - -template -std::array tuple_to_array(const std::tuple& t) noexcept -{ - return tuple_to_array_impl(t, boost::mp11::make_index_sequence()); -} - -template -class stmt_execute_it_serialize_fn -{ - std::uint32_t stmt_id_; - FieldViewFwdIterator first_; - FieldViewFwdIterator last_; - -public: - stmt_execute_it_serialize_fn( - std::uint32_t stmt_id, - FieldViewFwdIterator first, - FieldViewFwdIterator last - ) noexcept(std::is_nothrow_copy_constructible::value) - : stmt_id_(stmt_id), first_(first), last_(last) - { - } - - void operator()(capabilities caps, std::vector& buffer) const - { - auto request = make_stmt_execute_packet(stmt_id_, first_, last_); - serialize_message(request, caps, buffer); - } -}; - -template -class stmt_execute_tuple_serialize_fn -{ - std::uint32_t stmt_id_; - FieldLikeTuple params_; - -public: - // We need a deduced context to enable perfect forwarding - template - stmt_execute_tuple_serialize_fn(std::uint32_t stmt_id, DeducedTuple&& params) noexcept( - std::is_nothrow_constructible(params))>::value - ) - : stmt_id_(stmt_id), params_(std::forward(params)) - { - } - - void operator()(capabilities caps, std::vector& buffer) const - { - auto field_views = tuple_to_array(params_); - auto request = make_stmt_execute_packet(stmt_id_, field_views.begin(), field_views.end()); - serialize_message(request, caps, buffer); - } -}; - -inline error_code check_num_params(const statement& stmt, std::size_t param_count) -{ - if (param_count != stmt.num_params()) - { - return make_error_code(client_errc::wrong_num_params); - } - else - { - return error_code(); - } -} - -template -error_code check_num_params( - const statement& stmt, - FieldViewFwdIterator params_first, - FieldViewFwdIterator params_last -) -{ - return check_num_params(stmt, std::distance(params_first, params_last)); -} - -struct fast_fail_op : boost::asio::coroutine -{ - error_code err_; - diagnostics& diag_; - - fast_fail_op(error_code err, diagnostics& diag) noexcept : err_(err), diag_(diag) {} - - template - void operator()(Self& self) - { - BOOST_ASIO_CORO_REENTER(*this) - { - BOOST_ASIO_CORO_YIELD boost::asio::post(std::move(self)); - diag_.clear(); - self.complete(err_); - } - } -}; - -} // namespace detail -} // namespace mysql -} // namespace boost - -template -void boost::mysql::detail::start_statement_execution( - channel& chan, - const statement& stmt, - FieldViewFwdIterator params_first, - FieldViewFwdIterator params_last, - execution_state& output, - error_code& err, - diagnostics& diag -) -{ - err = check_num_params(stmt, params_first, params_last); - if (!err) - { - start_execution_generic( - resultset_encoding::binary, - chan, - stmt_execute_it_serialize_fn(stmt.id(), params_first, params_last), - output, - err, - diag - ); - } -} - -template < - class Stream, - BOOST_MYSQL_FIELD_VIEW_FORWARD_ITERATOR FieldViewFwdIterator, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_start_statement_execution( - channel& chan, - const statement& stmt, - FieldViewFwdIterator params_first, - FieldViewFwdIterator params_last, - execution_state& output, - diagnostics& diag, - CompletionToken&& token -) -{ - error_code err = check_num_params(stmt, params_first, params_last); - if (err) - { - return boost::asio::async_compose( - fast_fail_op(err, diag), - token, - chan - ); - } - return async_start_execution_generic( - resultset_encoding::binary, - chan, - stmt_execute_it_serialize_fn(stmt.id(), params_first, params_last), - output, - diag, - std::forward(token) - ); -} - -template -void boost::mysql::detail::start_statement_execution( - channel& channel, - const statement& stmt, - const FieldLikeTuple& params, - execution_state& output, - error_code& err, - diagnostics& diag -) -{ - auto params_array = tuple_to_array(params); - start_statement_execution(channel, stmt, params_array.begin(), params_array.end(), output, err, diag); -} - -template < - class Stream, - BOOST_MYSQL_FIELD_LIKE_TUPLE FieldLikeTuple, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) -boost::mysql::detail::async_start_statement_execution( - channel& chan, - const statement& stmt, - FieldLikeTuple&& params, - execution_state& output, - diagnostics& diag, - CompletionToken&& token -) -{ - using decayed_tuple = typename std::decay::type; - error_code err = check_num_params(stmt, std::tuple_size::value); - if (err) - { - return boost::asio::async_compose( - fast_fail_op(err, diag), - token, - chan - ); - } - return async_start_execution_generic( - resultset_encoding::binary, - chan, - stmt_execute_tuple_serialize_fn(stmt.id(), std::forward(params)), - output, - diag, - std::forward(token) - ); -} - -#endif /* INCLUDE_BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_IMPL_EXECUTE_STATEMENT_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/read_all_rows.hpp b/include/boost/mysql/detail/network_algorithms/read_resultset_head.hpp similarity index 62% rename from include/boost/mysql/detail/network_algorithms/read_all_rows.hpp rename to include/boost/mysql/detail/network_algorithms/read_resultset_head.hpp index effd37f1..f69913bf 100644 --- a/include/boost/mysql/detail/network_algorithms/read_all_rows.hpp +++ b/include/boost/mysql/detail/network_algorithms/read_resultset_head.hpp @@ -5,36 +5,33 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_READ_ALL_ROWS_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_READ_ALL_ROWS_HPP +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_READ_RESULTSET_HEAD_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_READ_RESULTSET_HEAD_HPP #include #include -#include -#include -#include +#include #include +#include namespace boost { namespace mysql { namespace detail { template -void read_all_rows( +void read_resultset_head( channel& channel, - execution_state& st, - rows& output, + execution_state_impl& st, error_code& err, diagnostics& diag ); template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) -async_read_all_rows( +async_read_resultset_head( channel& channel, - execution_state& st, - rows& output, + execution_state_impl& st, diagnostics& diag, CompletionToken&& token ); @@ -43,6 +40,6 @@ async_read_all_rows( } // namespace mysql } // namespace boost -#include +#include -#endif /* INCLUDE_MYSQL_IMPL_NETWORK_ALGORITHMS_READ_TEXT_ROW_HPP_ */ +#endif diff --git a/include/boost/mysql/detail/network_algorithms/query.hpp b/include/boost/mysql/detail/network_algorithms/start_execution.hpp similarity index 52% rename from include/boost/mysql/detail/network_algorithms/query.hpp rename to include/boost/mysql/detail/network_algorithms/start_execution.hpp index 51993ec7..88112402 100644 --- a/include/boost/mysql/detail/network_algorithms/query.hpp +++ b/include/boost/mysql/detail/network_algorithms/start_execution.hpp @@ -5,15 +5,15 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_QUERY_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_QUERY_HPP +#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_EXECUTION_HPP +#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_EXECUTION_HPP #include #include -#include -#include +#include #include +#include #include @@ -21,15 +21,24 @@ namespace boost { namespace mysql { namespace detail { +// The caller function must serialize the execution request into channel's buffer +// before calling these + template -void query(channel& channel, string_view query, results& output, error_code& err, diagnostics& diag); +void start_execution( + channel& channel, + resultset_encoding encoding, + execution_state& st, + error_code& err, + diagnostics& diag +); template BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) -async_query( +async_start_execution( channel& chan, - string_view query, - results& output, + resultset_encoding encoding, + execution_state& st, diagnostics& diag, CompletionToken&& token ); @@ -38,6 +47,6 @@ async_query( } // namespace mysql } // namespace boost -#include +#include -#endif +#endif /* INCLUDE_MYSQL_IMPL_NETWORK_ALGORITHMS_READ_RESULTSET_HEAD_HPP_ */ diff --git a/include/boost/mysql/detail/network_algorithms/start_execution_generic.hpp b/include/boost/mysql/detail/network_algorithms/start_execution_generic.hpp deleted file mode 100644 index 1468c18f..00000000 --- a/include/boost/mysql/detail/network_algorithms/start_execution_generic.hpp +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_EXECUTION_GENERIC_HPP -#define BOOST_MYSQL_DETAIL_NETWORK_ALGORITHMS_START_EXECUTION_GENERIC_HPP - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#ifdef BOOST_MYSQL_HAS_CONCEPTS -#include -#endif - -namespace boost { -namespace mysql { -namespace detail { - -// Exposed for the sake of testing -struct execute_response -{ - enum class type_t - { - num_fields, - ok_packet, - error - } type; - union data_t - { - static_assert(std::is_trivially_destructible::value, ""); - - std::size_t num_fields; - ok_packet ok_pack; - error_code err; - - data_t(size_t v) noexcept : num_fields(v) {} - data_t(const ok_packet& v) noexcept : ok_pack(v) {} - data_t(error_code v) noexcept : err(v) {} - } data; - - execute_response(std::size_t v) noexcept : type(type_t::num_fields), data(v) {} - execute_response(const ok_packet& v) noexcept : type(type_t::ok_packet), data(v) {} - execute_response(error_code v) noexcept : type(type_t::error), data(v) {} -}; -inline execute_response deserialize_execute_response( - boost::asio::const_buffer msg, - capabilities caps, - db_flavor flavor, - diagnostics& diag -) noexcept; - -#ifdef BOOST_MYSQL_HAS_CONCEPTS - -template -concept serialize_fn = std::invocable, capabilities, std::vector&>; - -#define BOOST_MYSQL_SERIALIZE_FN ::boost::mysql::detail::serialize_fn - -#else // BOOST_MYSQL_HAS_CONCEPTS - -#define BOOST_MYSQL_SERIALIZE_FN class - -#endif // BOOST_MYSQL_HAS_CONCEPTS - -// The sync version gets passed directlty the request packet to be serialized. -// There is no need to defer the serialization here. -template -void start_execution_generic( - resultset_encoding encoding, - channel& channel, - const SerializeFn& fn, - execution_state& st, - error_code& err, - diagnostics& diag -); - -// The async version gets passed a request maker, holding enough data to create -// the request packet when the async op is started. Used by statement execution -// with tuple params, to make lifetimes more user-friendly when using deferred tokens. -template < - class Stream, - BOOST_MYSQL_SERIALIZE_FN SerializeFn, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code)) CompletionToken> -BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code)) -async_start_execution_generic( - resultset_encoding encoding, - channel& chan, - SerializeFn&& fn, - execution_state& st, - diagnostics& diag, - CompletionToken&& token -); - -} // namespace detail -} // namespace mysql -} // namespace boost - -#include - -#endif /* INCLUDE_MYSQL_IMPL_NETWORK_ALGORITHMS_READ_RESULTSET_HEAD_HPP_ */ diff --git a/include/boost/mysql/detail/protocol/capabilities.hpp b/include/boost/mysql/detail/protocol/capabilities.hpp index ae980da9..912d869f 100644 --- a/include/boost/mysql/detail/protocol/capabilities.hpp +++ b/include/boost/mysql/detail/protocol/capabilities.hpp @@ -67,14 +67,8 @@ public: { return capabilities(value_ & rhs.value_); } - constexpr bool operator==(const capabilities& rhs) const noexcept - { - return value_ == rhs.value_; - } - constexpr bool operator!=(const capabilities& rhs) const noexcept - { - return value_ != rhs.value_; - } + constexpr bool operator==(const capabilities& rhs) const noexcept { return value_ == rhs.value_; } + constexpr bool operator!=(const capabilities& rhs) const noexcept { return value_ != rhs.value_; } }; /* @@ -127,7 +121,7 @@ constexpr capabilities mandatory_capabilities{ }; // clang-format on -constexpr capabilities optional_capabilities{0}; +constexpr capabilities optional_capabilities{CLIENT_MULTI_RESULTS | CLIENT_PS_MULTI_RESULTS}; } // namespace detail } // namespace mysql diff --git a/include/boost/mysql/detail/protocol/deserialize_binary_field.hpp b/include/boost/mysql/detail/protocol/deserialize_binary_field.hpp index 11aaa309..43a09d0f 100644 --- a/include/boost/mysql/detail/protocol/deserialize_binary_field.hpp +++ b/include/boost/mysql/detail/protocol/deserialize_binary_field.hpp @@ -24,7 +24,6 @@ namespace detail { inline deserialize_errc deserialize_binary_field( deserialization_context& ctx, const metadata& meta, - const std::uint8_t* buffer_first, field_view& output ); diff --git a/include/boost/mysql/detail/protocol/deserialize_execute_response.hpp b/include/boost/mysql/detail/protocol/deserialize_execute_response.hpp new file mode 100644 index 00000000..c2e69f42 --- /dev/null +++ b/include/boost/mysql/detail/protocol/deserialize_execute_response.hpp @@ -0,0 +1,62 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_PROTOCOL_DESERIALIZE_EXECUTE_RESPONSE_HPP +#define BOOST_MYSQL_DETAIL_PROTOCOL_DESERIALIZE_EXECUTE_RESPONSE_HPP + +#include +#include + +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Exposed here for the sake of testing +struct execute_response +{ + enum class type_t + { + num_fields, + ok_packet, + error + } type; + union data_t + { + static_assert(std::is_trivially_destructible::value, ""); + + std::size_t num_fields; + ok_packet ok_pack; + error_code err; + + data_t(size_t v) noexcept : num_fields(v) {} + data_t(const ok_packet& v) noexcept : ok_pack(v) {} + data_t(error_code v) noexcept : err(v) {} + } data; + + execute_response(std::size_t v) noexcept : type(type_t::num_fields), data(v) {} + execute_response(const ok_packet& v) noexcept : type(type_t::ok_packet), data(v) {} + execute_response(error_code v) noexcept : type(type_t::error), data(v) {} +}; + +inline execute_response deserialize_execute_response( + boost::asio::const_buffer msg, + capabilities caps, + db_flavor flavor, + diagnostics& diag +) noexcept; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#include + +#endif diff --git a/include/boost/mysql/detail/protocol/deserialize_row.hpp b/include/boost/mysql/detail/protocol/deserialize_row.hpp index d8b93b19..8255b3e6 100644 --- a/include/boost/mysql/detail/protocol/deserialize_row.hpp +++ b/include/boost/mysql/detail/protocol/deserialize_row.hpp @@ -12,10 +12,12 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -27,13 +29,11 @@ namespace boost { namespace mysql { namespace detail { -// Exposed here for the sake of testing inline void deserialize_row( resultset_encoding encoding, deserialization_context& ctx, - const std::vector& meta, - const std::uint8_t* buffer_first, - std::vector& output, + metadata_collection_view meta, + field_view* output, // Should point to meta.size() field_view objects error_code& err ); @@ -41,22 +41,11 @@ inline void deserialize_row( boost::asio::const_buffer read_message, capabilities current_capabilities, db_flavor flavor, - const std::uint8_t* buffer_first, // to store strings as offsets and allow buffer reallocation - execution_state& st, // should be !complete() - std::vector& output, + execution_state_impl& st, error_code& err, diagnostics& diag ); -inline void offsets_to_string_views( - std::vector& fields, - const std::uint8_t* buffer_first -) noexcept -{ - for (auto& f : fields) - field_view_access::offset_to_string_view(f, buffer_first); -} - } // namespace detail } // namespace mysql } // namespace boost diff --git a/include/boost/mysql/detail/protocol/deserialize_text_field.hpp b/include/boost/mysql/detail/protocol/deserialize_text_field.hpp index 23873aa6..befe857c 100644 --- a/include/boost/mysql/detail/protocol/deserialize_text_field.hpp +++ b/include/boost/mysql/detail/protocol/deserialize_text_field.hpp @@ -21,12 +21,7 @@ namespace boost { namespace mysql { namespace detail { -inline deserialize_errc deserialize_text_field( - string_view from, - const metadata& meta, - const std::uint8_t* buffer_first, - field_view& output -); +inline deserialize_errc deserialize_text_field(string_view from, const metadata& meta, field_view& output); } // namespace detail } // namespace mysql diff --git a/include/boost/mysql/detail/protocol/execution_state_impl.hpp b/include/boost/mysql/detail/protocol/execution_state_impl.hpp new file mode 100644 index 00000000..bba58f1d --- /dev/null +++ b/include/boost/mysql/detail/protocol/execution_state_impl.hpp @@ -0,0 +1,438 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_PROTOCOL_EXECUTION_STATE_IMPL_HPP +#define BOOST_MYSQL_DETAIL_PROTOCOL_EXECUTION_STATE_IMPL_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// A rows-like collection that can hold the rows for multiple resultsets. +// This class also helps gathering all the rows without incurring in dangling references. +// Rows are read in batches: +// - Open a batch calling start_batch() +// - Call deserialize_row(), which will call add_row(). This adds raw storage for fields. +// Fields are then deserialized there. string/blob field_view's point into the +// connection's internal buffer. +// - When the batch finishes, call finish_batch(). This will copy strings into thr row_impl +// and transform these values into offsets, so the buffer can be grown. +// - When the final OK packet is received, offsets are converted back into views, +// by calling finish() +class multi_rows +{ + static constexpr std::size_t no_batch = std::size_t(-1); + + row_impl impl_; + std::size_t num_fields_at_batch_start_{no_batch}; + + bool has_active_batch() const noexcept { return num_fields_at_batch_start_ != no_batch; } + +public: + multi_rows() = default; + + std::size_t num_fields() const noexcept { return impl_.fields().size(); } + + rows_view rows_slice(std::size_t offset, std::size_t num_columns, std::size_t num_rows) const noexcept + { + return rows_view_access::construct( + impl_.fields().data() + offset, + num_rows * num_columns, + num_columns + ); + } + + void reset() noexcept + { + impl_.clear(); + num_fields_at_batch_start_ = no_batch; + } + + void start_batch() noexcept + { + assert(!has_active_batch()); + num_fields_at_batch_start_ = num_fields(); + } + + field_view* add_row(std::size_t num_fields) + { + assert(has_active_batch()); + return impl_.add_fields(num_fields); + } + + void finish_batch() + { + if (has_active_batch()) + { + impl_.copy_strings_as_offsets( + num_fields_at_batch_start_, + impl_.fields().size() - num_fields_at_batch_start_ + ); + num_fields_at_batch_start_ = no_batch; + } + } + + void finish() + { + finish_batch(); + impl_.offsets_to_string_views(); + } +}; + +struct per_resultset_data +{ + std::size_t num_columns{}; // Number of columns this resultset has + std::size_t meta_offset{}; // Offset into the vector of metadata + std::size_t field_offset; // Offset into the vector of fields (append mode only) + std::size_t num_rows{}; // Number of rows this resultset has (append mode only) + std::uint64_t affected_rows{}; // OK packet data + std::uint64_t last_insert_id{}; // OK packet data + std::uint16_t warnings{}; // OK packet data + std::size_t info_offset{}; // Offset into the vector of info characters + std::size_t info_size{}; // Number of characters that this resultset's info string has + bool has_ok_packet_data{false}; // The OK packet information is default constructed, or actual data? + bool is_out_params{false}; // Does this resultset contain OUT param information? +}; + +// A container similar to a vector with SBO. To avoid depending on Boost.Container +class resultset_container +{ + bool first_has_data_{false}; + per_resultset_data first_; + std::vector rest_; + +public: + resultset_container() = default; + std::size_t size() const noexcept { return !first_has_data_ ? 0 : rest_.size() + 1; } + bool empty() const noexcept { return !first_has_data_; } + void clear() noexcept + { + first_has_data_ = false; + rest_.clear(); + } + per_resultset_data& operator[](std::size_t i) noexcept + { + return const_cast(const_cast(*this)[i]); + } + const per_resultset_data& operator[](std::size_t i) const noexcept + { + assert(i < size()); + return i == 0 ? first_ : rest_[i - 1]; + } + per_resultset_data& back() noexcept + { + return const_cast(const_cast(*this).back()); + } + const per_resultset_data& back() const noexcept + { + assert(first_has_data_); + return rest_.empty() ? first_ : rest_.back(); + } + per_resultset_data& emplace_back() + { + if (!first_has_data_) + { + first_ = per_resultset_data(); + first_has_data_ = true; + return first_; + } + else + { + rest_.emplace_back(); + return rest_.back(); + } + } +}; + +class execution_state_impl +{ +public: + // Append mode=true is used by single-function operations. Metadata and info are + // appended to the collections here. Append mode=false is used by multi-function + // operations. Every new resultset wipes the previous one + // In append mode, rows are owned by this class. In non-append mode, + // they're owned by the external storage (usually the channel). + execution_state_impl(bool append_mode) noexcept : append_mode_(append_mode) {} + + bool is_append_mode() const noexcept { return append_mode_; } + + // State accessors + bool should_read_head() const noexcept { return state_ == state_t::reading_first_packet; } + bool should_read_meta() const noexcept { return state_ == state_t::reading_metadata; } + bool should_read_rows() const noexcept { return state_ == state_t::reading_rows; } + bool complete() const noexcept { return state_ == state_t::complete; } + + // State transitions + void reset(detail::resultset_encoding encoding, std::vector* field_storage) noexcept + { + state_ = state_t::reading_first_packet; + seqnum_ = 0; + encoding_ = encoding; + remaining_meta_ = 0; + meta_.clear(); + per_result_.clear(); + info_.clear(); + if (append_mode_) + { + rows_.reset(); + } + else + { + assert(field_storage != nullptr); + external_rows_ = field_storage; + } + } + + void on_num_meta(std::size_t num_columns) + { + assert(state_ == state_t::reading_first_packet); + on_new_resultset(); + meta_.reserve(meta_.size() + num_columns); + auto& resultset_data = current_resultset(); + resultset_data.num_columns = num_columns; + remaining_meta_ = num_columns; + state_ = state_t::reading_metadata; + } + + void on_meta(const detail::column_definition_packet& pack, metadata_mode mode) + { + assert(state_ == state_t::reading_metadata); + meta_.push_back(metadata_access::construct(pack, mode == metadata_mode::full)); + if (--remaining_meta_ == 0) + { + state_ = state_t::reading_rows; + } + } + + void on_row_batch_start() noexcept + { + assert(state_ == state_t::reading_rows); + if (append_mode_) + { + rows_.start_batch(); + } + else + { + external_rows_->clear(); + } + } + + field_view* add_row() + { + assert(state_ == state_t::reading_rows); + std::size_t num_fields = current_resultset().num_columns; + field_view* res = append_mode_ ? rows_.add_row(num_fields) : add_fields(*external_rows_, num_fields); + ++current_resultset().num_rows; + return res; + } + + void on_row_batch_finish() + { + // it's legal to invoke this function from any state + if (append_mode_) + { + rows_.finish_batch(); + } + } + + // When we receive an OK packet in an execute function, as the first packet + void on_head_ok_packet(const ok_packet& pack) + { + assert(state_ == state_t::reading_first_packet); + on_new_resultset(); + on_ok_packet_impl(pack); + } + + // When we receive an OK packet while reading rows + void on_row_ok_packet(const ok_packet& pack) + { + assert(state_ == state_t::reading_rows); + on_ok_packet_impl(pack); + } + + // Accessors for other protocol stuff + resultset_encoding encoding() const noexcept { return encoding_; } + std::uint8_t& sequence_number() noexcept { return seqnum_; } + + metadata_collection_view current_resultset_meta() const noexcept + { + assert(state_ == state_t::reading_rows); + return get_meta(per_result_.size() - 1); + } + + // Accessors for user-facing components + std::size_t num_resultsets() const noexcept { return per_result_.size(); } + + rows_view get_rows(std::size_t index) const noexcept + { + assert(append_mode_); + const auto& resultset_data = per_result_[index]; + return rows_ + .rows_slice(resultset_data.field_offset, resultset_data.num_columns, resultset_data.num_rows); + } + + rows_view get_external_rows() const noexcept + { + assert(!append_mode_); + return rows_view_access::construct( + external_rows_->data(), + external_rows_->size(), + current_resultset().num_columns + ); + } + + metadata_collection_view get_meta(std::size_t index) const noexcept + { + const auto& resultset_data = per_result_[index]; + return metadata_collection_view( + meta_.data() + resultset_data.meta_offset, + resultset_data.num_columns + ); + } + + std::uint64_t get_affected_rows(std::size_t index) const noexcept + { + return get_resultset_with_ok_packet(index).affected_rows; + } + + std::uint64_t get_last_insert_id(std::size_t index) const noexcept + { + return get_resultset_with_ok_packet(index).last_insert_id; + } + + unsigned get_warning_count(std::size_t index) const noexcept + { + return get_resultset_with_ok_packet(index).warnings; + } + + string_view get_info(std::size_t index) const noexcept + { + const auto& resultset_data = get_resultset_with_ok_packet(index); + return string_view(info_.data() + resultset_data.info_offset, resultset_data.info_size); + } + + bool get_is_out_params(std::size_t index) const noexcept + { + return get_resultset_with_ok_packet(index).is_out_params; + } + + row_view get_out_params() const noexcept + { + assert(append_mode_ && state_ == state_t::complete); + for (std::size_t i = 0; i < per_result_.size(); ++i) + { + if (per_result_[i].is_out_params) + { + auto res = get_rows(i); + return res.empty() ? row_view() : res[0]; + } + } + return row_view(); + } + +private: + enum class state_t + { + reading_first_packet, // we're waiting for a resultset's 1st packet + reading_metadata, + reading_rows, + complete + }; + + void on_new_resultset() noexcept + { + // Clean stuff from previous resultsets if we're not in append mode + if (!append_mode_) + { + meta_.clear(); + per_result_.clear(); + info_.clear(); + } + + // Allocate a new per-resultset object + auto& resultset_data = per_result_.emplace_back(); + resultset_data.meta_offset = meta_.size(); + resultset_data.field_offset = rows_.num_fields(); + resultset_data.info_offset = info_.size(); + } + + void on_ok_packet_impl(const ok_packet& pack) noexcept + { + auto& resultset_data = current_resultset(); + resultset_data.affected_rows = pack.affected_rows.value; + resultset_data.last_insert_id = pack.last_insert_id.value; + resultset_data.warnings = pack.warnings; + resultset_data.info_size = pack.info.value.size(); + resultset_data.has_ok_packet_data = true; + resultset_data.is_out_params = pack.status_flags & SERVER_PS_OUT_PARAMS; + info_.insert(info_.end(), pack.info.value.begin(), pack.info.value.end()); + if (pack.status_flags & SERVER_MORE_RESULTS_EXISTS) + { + state_ = state_t::reading_first_packet; + } + else + { + if (append_mode_) + { + rows_.finish(); + } + state_ = state_t::complete; + } + } + + per_resultset_data& current_resultset() noexcept + { + assert(!per_result_.empty()); + return per_result_.back(); + } + + const per_resultset_data& current_resultset() const noexcept + { + assert(!per_result_.empty()); + return per_result_.back(); + } + + const per_resultset_data& get_resultset_with_ok_packet(std::size_t index) const noexcept + { + const auto& res = per_result_[index]; + assert(res.has_ok_packet_data); + return res; + } + + bool append_mode_{false}; + state_t state_{state_t::reading_first_packet}; + std::uint8_t seqnum_{}; + detail::resultset_encoding encoding_{detail::resultset_encoding::text}; + std::size_t remaining_meta_{}; + std::vector meta_; + resultset_container per_result_; + std::vector info_; + multi_rows rows_; // if append_mode + std::vector* external_rows_{}; // if !append_mode +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/detail/protocol/impl/deserialize_binary_field.ipp b/include/boost/mysql/detail/protocol/impl/deserialize_binary_field.ipp index 56a64091..2570de26 100644 --- a/include/boost/mysql/detail/protocol/impl/deserialize_binary_field.ipp +++ b/include/boost/mysql/detail/protocol/impl/deserialize_binary_field.ipp @@ -29,7 +29,6 @@ namespace detail { inline deserialize_errc deserialize_binary_field_string( deserialization_context& ctx, field_view& output, - const std::uint8_t* buffer_first, bool is_blob ) noexcept { @@ -37,10 +36,16 @@ inline deserialize_errc deserialize_binary_field_string( auto err = deserialize(ctx, deser); if (err != deserialize_errc::ok) return err; - output = detail::field_view_access::construct( - detail::string_view_offset::from_sv(deser.value, buffer_first), - is_blob - ); + if (is_blob) + { + output = field_view( + blob_view(reinterpret_cast(deser.value.data()), deser.value.size()) + ); + } + else + { + output = field_view(deser.value); + } return deserialize_errc::ok; } @@ -269,7 +274,6 @@ inline deserialize_errc deserialize_binary_field_time( inline boost::mysql::detail::deserialize_errc boost::mysql::detail::deserialize_binary_field( deserialization_context& ctx, const metadata& meta, - const std::uint8_t* buffer_first, field_view& output ) { @@ -299,13 +303,13 @@ inline boost::mysql::detail::deserialize_errc boost::mysql::detail::deserialize_ case column_type::enum_: case column_type::set: case column_type::decimal: - case column_type::json: return deserialize_binary_field_string(ctx, output, buffer_first, false); + case column_type::json: return deserialize_binary_field_string(ctx, output, false); // Blobs and anything else case column_type::binary: case column_type::varbinary: case column_type::blob: case column_type::geometry: - default: return deserialize_binary_field_string(ctx, output, buffer_first, true); + default: return deserialize_binary_field_string(ctx, output, true); } } diff --git a/include/boost/mysql/detail/protocol/impl/deserialize_execute_response.ipp b/include/boost/mysql/detail/protocol/impl/deserialize_execute_response.ipp new file mode 100644 index 00000000..7c02d9cf --- /dev/null +++ b/include/boost/mysql/detail/protocol/impl/deserialize_execute_response.ipp @@ -0,0 +1,76 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_EXECUTE_RESPONSE_IPP +#define BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_EXECUTE_RESPONSE_IPP + +#pragma once + +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +} // namespace detail +} // namespace mysql +} // namespace boost + +inline boost::mysql::detail::execute_response boost::mysql::detail::deserialize_execute_response( + boost::asio::const_buffer msg, + capabilities caps, + db_flavor flavor, + diagnostics& diag +) noexcept +{ + // Response may be: ok_packet, err_packet, local infile request (not implemented) + // If it is none of this, then the message type itself is the beginning of + // a length-encoded int containing the field count + deserialization_context ctx(msg, caps); + std::uint8_t msg_type = 0; + auto err = deserialize_message_part(ctx, msg_type); + if (err) + return err; + + if (msg_type == ok_packet_header) + { + ok_packet ok_pack; + err = deserialize_message(ctx, ok_pack); + if (err) + return err; + return ok_pack; + } + else if (msg_type == error_packet_header) + { + return process_error_packet(ctx, flavor, diag); + } + else + { + // Resultset with metadata. First packet is an int_lenenc with + // the number of field definitions to expect. Message type is part + // of this packet, so we must rewind the context + ctx.rewind(1); + int_lenenc num_fields; + err = deserialize_message(ctx, num_fields); + if (err) + return err; + + // We should have at least one field. + // The max number of fields is some value around 1024. For simplicity/extensibility, + // we accept anything less than 0xffff + if (num_fields.value == 0 || num_fields.value > 0xffffu) + { + return make_error_code(client_errc::protocol_value_error); + } + + return static_cast(num_fields.value); + } +} + +#endif diff --git a/include/boost/mysql/detail/protocol/impl/deserialize_row.ipp b/include/boost/mysql/detail/protocol/impl/deserialize_row.ipp index c1250213..d8fd75d6 100644 --- a/include/boost/mysql/detail/protocol/impl/deserialize_row.ipp +++ b/include/boost/mysql/detail/protocol/impl/deserialize_row.ipp @@ -8,6 +8,9 @@ #ifndef BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_ROW_IPP #define BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_ROW_IPP +#include + +#include #pragma once #include @@ -30,22 +33,16 @@ inline bool is_next_field_null(const deserialization_context& ctx) inline error_code deserialize_text_row( deserialization_context& ctx, - const std::vector& fields, - const std::uint8_t* buffer_first, - std::vector& output + metadata_collection_view meta, + field_view* output ) { - // Make space - std::size_t old_size = output.size(); - auto num_fields = fields.size(); - output.resize(old_size + num_fields); - - for (std::vector::size_type i = 0; i < num_fields; ++i) + for (std::vector::size_type i = 0; i < meta.size(); ++i) { if (is_next_field_null(ctx)) { ctx.advance(1); - output[old_size + i] = field_view(nullptr); + output[i] = field_view(nullptr); } else { @@ -53,7 +50,7 @@ inline error_code deserialize_text_row( auto err = deserialize(ctx, value_str); if (err != deserialize_errc::ok) return to_error_code(err); - err = deserialize_text_field(value_str.value, fields[i], buffer_first, output[old_size + i]); + err = deserialize_text_field(value_str.value, meta[i], output[i]); if (err != deserialize_errc::ok) return to_error_code(err); } @@ -65,9 +62,8 @@ inline error_code deserialize_text_row( inline error_code deserialize_binary_row( deserialization_context& ctx, - const std::vector& meta, - const std::uint8_t* buffer_first, - std::vector& output + metadata_collection_view meta, + field_view* output ) { // Skip packet header (it is not part of the message in the binary @@ -77,9 +73,7 @@ inline error_code deserialize_binary_row( ctx.advance(1); // Number of fields - std::size_t old_size = output.size(); - auto num_fields = meta.size(); - output.resize(old_size + num_fields); + std::size_t num_fields = meta.size(); // Null bitmap null_bitmap_traits null_bitmap(binary_row_null_bitmap_offset, num_fields); @@ -93,11 +87,11 @@ inline error_code deserialize_binary_row( { if (null_bitmap.is_null(null_bitmap_begin, i)) { - output[old_size + i] = field_view(nullptr); + output[i] = field_view(nullptr); } else { - auto err = deserialize_binary_field(ctx, meta[i], buffer_first, output[old_size + i]); + auto err = deserialize_binary_field(ctx, meta[i], output[i]); if (err != deserialize_errc::ok) return to_error_code(err); } @@ -117,29 +111,25 @@ inline error_code deserialize_binary_row( void boost::mysql::detail::deserialize_row( resultset_encoding encoding, deserialization_context& ctx, - const std::vector& meta, - const std::uint8_t* buffer_first, - std::vector& output, + metadata_collection_view meta, + field_view* output, error_code& err ) { - err = encoding == detail::resultset_encoding::text - ? deserialize_text_row(ctx, meta, buffer_first, output) - : deserialize_binary_row(ctx, meta, buffer_first, output); + err = encoding == detail::resultset_encoding::text ? deserialize_text_row(ctx, meta, output) + : deserialize_binary_row(ctx, meta, output); } void boost::mysql::detail::deserialize_row( boost::asio::const_buffer read_message, capabilities current_capabilities, db_flavor flavor, - const std::uint8_t* buffer_first, // to store strings as offsets and allow buffer reallocation - execution_state& st, - std::vector& output, + execution_state_impl& st, error_code& err, diagnostics& diag ) { - assert(!st.complete()); + assert(st.should_read_rows()); // Message type: row, error or eof? std::uint8_t msg_type = 0; @@ -158,7 +148,7 @@ void boost::mysql::detail::deserialize_row( err = deserialize_message(ctx, ok_pack); if (err) return; - execution_state_access::complete(st, ok_pack); + st.on_row_ok_packet(ok_pack); } else if (msg_type == error_packet_header) { @@ -169,14 +159,8 @@ void boost::mysql::detail::deserialize_row( { // An actual row ctx.rewind(1); // keep the 'message type' byte, as it is part of the actual message - deserialize_row( - execution_state_access::get_encoding(st), - ctx, - execution_state_access::get_metadata(st), - buffer_first, - output, - err - ); + field_view* storage = st.add_row(); + deserialize_row(st.encoding(), ctx, st.current_resultset_meta(), storage, err); } } diff --git a/include/boost/mysql/detail/protocol/impl/deserialize_text_field.ipp b/include/boost/mysql/detail/protocol/impl/deserialize_text_field.ipp index 6a8c47fc..033d2b63 100644 --- a/include/boost/mysql/detail/protocol/impl/deserialize_text_field.ipp +++ b/include/boost/mysql/detail/protocol/impl/deserialize_text_field.ipp @@ -8,6 +8,7 @@ #ifndef BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_TEXT_FIELD_IPP #define BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DESERIALIZE_TEXT_FIELD_IPP +#include #pragma once #include @@ -68,14 +69,15 @@ deserialize_errc deserialize_text_value_float(string_view from, field_view& to) } // Strings -inline deserialize_errc deserialize_text_value_string( - string_view from, - field_view& to, - const std::uint8_t* buffer_first, - bool is_blob -) noexcept +inline deserialize_errc deserialize_text_value_string(string_view from, field_view& to) noexcept { - to = field_view_access::construct(string_view_offset::from_sv(from, buffer_first), is_blob); + to = field_view(from); + return deserialize_errc::ok; +} + +inline deserialize_errc deserialize_text_value_blob(string_view from, field_view& to) noexcept +{ + to = field_view(blob_view(reinterpret_cast(from.data()), from.size())); return deserialize_errc::ok; } @@ -269,7 +271,6 @@ inline deserialize_errc deserialize_text_value_time( inline boost::mysql::detail::deserialize_errc boost::mysql::detail::deserialize_text_field( string_view from, const metadata& meta, - const std::uint8_t* buffer_first, field_view& output ) { @@ -295,13 +296,13 @@ inline boost::mysql::detail::deserialize_errc boost::mysql::detail::deserialize_ case column_type::enum_: case column_type::set: case column_type::decimal: - case column_type::json: return deserialize_text_value_string(from, output, buffer_first, false); + case column_type::json: return deserialize_text_value_string(from, output); // Blobs and anything else case column_type::binary: case column_type::varbinary: case column_type::blob: case column_type::geometry: - default: return deserialize_text_value_string(from, output, buffer_first, true); + default: return deserialize_text_value_blob(from, output); } } diff --git a/include/boost/mysql/execution_state.hpp b/include/boost/mysql/execution_state.hpp index 3132cb58..ae7ff6b6 100644 --- a/include/boost/mysql/execution_state.hpp +++ b/include/boost/mysql/execution_state.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -26,6 +27,12 @@ namespace mysql { /** * \brief Holds state for multi-function SQL execution operations. + * \details + * This class behaves like a state machine. The current state can be accessed using + * \ref should_start_op, \ref should_read_rows, \ref should_read_head + * and \ref complete. They are mutually exclusive. + * More states may be added in the future as more protocol features are implemented. + * * \par Thread safety * Distinct objects: safe. \n * Shared objects: unsafe. @@ -35,24 +42,101 @@ class execution_state public: /** * \brief Default constructor. - * \details The constructed object is guaranteed to have `meta().empty()` and - * `!complete()`. + * \details The constructed object is guaranteed to have + * `should_start_op() == true`. * * \par Exception safety * No-throw guarantee. */ - execution_state() = default; + execution_state() noexcept : impl_(false){}; + + /** + * \brief Copy constructor. + * \par Exception safety + * Strong guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `other`'s. + */ + execution_state(const execution_state& other) = default; + + /** + * \brief Move constructor. + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * Views obtained from `other` remain valid. + */ + execution_state(execution_state&& other) = default; + + /** + * \brief Copy assignment. + * \par Exception safety + * Basic guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `other`'s. Views obtained from `*this` + * are invalidated. + */ + execution_state& operator=(const execution_state& other) = default; + + /** + * \brief Move assignment. + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * Views obtained from `*this` are invalidated. Views obtained from `other` remain valid. + */ + execution_state& operator=(execution_state&& other) = default; + + /** + * \brief Returns whether `*this` is in the initial state. + * \details + * Call `start_query`, `start_statement_execution` or their async countrparts to move + * forward. No data is available in this state. + * + * \par Exception safety + * No-throw guarantee. + */ + bool should_start_op() const noexcept { return impl_.num_resultsets() == 0u; } + + /** + * \brief Returns whether the next operation should be read resultset head. + * \details + * Call \ref connection::read_resultset_head or its async counterpart to move forward. + * Metadata and OK data for the previous resultset is available in this state. + * + * \par Exception safety + * No-throw guarantee. + */ + bool should_read_head() const noexcept + { + return impl_.num_resultsets() == 1u && (impl_.should_read_head() || impl_.should_read_meta()); + } + + /** + * \brief Returns whether the next operation should be read some rows. + * \details + * Call \ref connection::read_some_rows or its async counterpart to move forward. + * Metadata for the current resultset is available in this state. + * + * \par Exception safety + * No-throw guarantee. + */ + bool should_read_rows() const noexcept { return impl_.should_read_rows(); } /** * \brief Returns whether all the messages generated by this operation have been read. * \details - * Once `complete`, you may access extra information about the operation, like - * \ref affected_rows or \ref last_insert_id. + * No further network calls are required to move forward. Metadata and OK data for the last + * resultset are available in this state. * * \par Exception safety * No-throw guarantee. */ - bool complete() const noexcept { return eof_received_; } + bool complete() const noexcept { return impl_.complete(); } /** * \brief Returns metadata about the columns in the query. @@ -70,53 +154,41 @@ public: */ metadata_collection_view meta() const noexcept { - return metadata_collection_view(meta_.data(), meta_.size()); + return impl_.num_resultsets() == 0u ? metadata_collection_view() : impl_.get_meta(0); } /** - * \brief Returns the number of rows affected by the executed SQL statement. + * \brief Returns the number of rows affected by the SQL statement associated to this resultset. * \par Exception safety * No-throw guarantee. * * \par Preconditions - * `this->complete() == true` + * `this->complete() == true || this->should_read_head() == true` */ - std::uint64_t affected_rows() const noexcept - { - assert(complete()); - return affected_rows_; - } + std::uint64_t affected_rows() const noexcept { return impl_.get_affected_rows(0); } /** - * \brief Returns the last insert ID produced by the executed SQL statement. + * \brief Returns the last insert ID produced by the SQL statement associated to this resultset. * \par Exception safety * No-throw guarantee. * * \par Preconditions - * `this->complete() == true` + * `this->complete() == true || this->should_read_head() == true` */ - std::uint64_t last_insert_id() const noexcept - { - assert(complete()); - return last_insert_id_; - } + std::uint64_t last_insert_id() const noexcept { return impl_.get_last_insert_id(0); } /** - * \brief Returns the number of warnings produced by the executed SQL statement. + * \brief Returns the number of warnings produced by the SQL statement associated to this resultset. * \par Exception safety * No-throw guarantee. * * \par Preconditions - * `this->complete() == true` + * `this->complete() == true || this->should_read_head() == true` */ - unsigned warning_count() const noexcept - { - assert(complete()); - return warnings_; - } + unsigned warning_count() const noexcept { return impl_.get_warning_count(0); } /** - * \brief Returns additionat text information about the execution of the SQL statement. + * \brief Returns additionat text information about this resultset. * \details * The format of this information is documented by MySQL here. @@ -127,37 +199,36 @@ public: * No-throw guarantee. * * \par Preconditions - * `this->complete() == true` + * `this->complete() == true || this->should_read_head() == true` * * \par Object lifetimes * This function returns a view object, with reference semantics. The returned view points into * memory owned by `*this`, and will be valid as long as `*this` or an object move-constructed * from `*this` are alive. */ - string_view info() const noexcept - { - assert(complete()); - return string_view(info_.data(), info_.size()); - } + string_view info() const noexcept { return impl_.get_info(0); } + + /** + * \brief Returns whether the current resultset represents a procedure OUT params. + * \par Preconditions + * `this->complete() == true || this->should_read_head() == true` + * + * \par Exception safety + * No-throw guarantee. + */ + bool is_out_params() const noexcept { return impl_.get_is_out_params(0); } private: - bool eof_received_{false}; - std::uint8_t seqnum_{}; - detail::resultset_encoding encoding_{detail::resultset_encoding::text}; - std::vector meta_; - std::uint64_t affected_rows_{}; - std::uint64_t last_insert_id_{}; - std::uint16_t warnings_{}; - std::vector info_; // guarantee that no SBO is used + detail::execution_state_impl impl_; #ifndef BOOST_MYSQL_DOXYGEN friend struct detail::execution_state_access; #endif }; +#include + } // namespace mysql } // namespace boost -#include - #endif diff --git a/include/boost/mysql/handshake_params.hpp b/include/boost/mysql/handshake_params.hpp index 9b0d6bae..1812b970 100644 --- a/include/boost/mysql/handshake_params.hpp +++ b/include/boost/mysql/handshake_params.hpp @@ -30,6 +30,7 @@ class handshake_params string_view database_; std::uint16_t connection_collation_; ssl_mode ssl_; + bool multi_queries_; public: /// The default collation to use with the connection (`utf8mb4_general_ci` on both MySQL and MariaDB). @@ -48,19 +49,23 @@ public: * `utf8mb4_general_ci` (see \ref default_collation), which is compatible with MySQL 5.x, 8.x and MariaDB. * \param mode The \ref ssl_mode to use with this connection; ignored if * the connection's `Stream` does not support SSL. + * \param multi_queries Whether to enable support for executing semicolon-separated + * queries using \ref connection::query. Disabled by default. */ handshake_params( string_view username, string_view password, string_view db = "", std::uint16_t connection_col = default_collation, - ssl_mode mode = ssl_mode::require + ssl_mode mode = ssl_mode::require, + bool multi_queries = false ) : username_(username), password_(password), database_(db), connection_collation_(connection_col), - ssl_(mode) + ssl_(mode), + multi_queries_(multi_queries) { } @@ -133,6 +138,20 @@ public: * No-throw guarantee. */ void set_ssl(ssl_mode value) noexcept { ssl_ = value; } + + /** + * \brief Retrieves whether multi-query support is enabled. + * \par Exception safety + * No-throw guarantee. + */ + bool multi_queries() const noexcept { return multi_queries_; } + + /** + * \brief Enables or disables support for the multi-query feature. + * \par Exception safety + * No-throw guarantee. + */ + void set_multi_queries(bool v) noexcept { multi_queries_ = v; } }; } // namespace mysql diff --git a/include/boost/mysql/impl/connection.hpp b/include/boost/mysql/impl/connection.hpp index fa69c10b..41026632 100644 --- a/include/boost/mysql/impl/connection.hpp +++ b/include/boost/mysql/impl/connection.hpp @@ -21,15 +21,13 @@ #include #include #include -#include #include +#include #include #include -#include #include +#include #include -#include -#include #include #include @@ -416,6 +414,53 @@ boost::mysql::connection::async_close_statement( return detail::async_close_statement(get_channel(), stmt, diag, std::forward(token)); } +// read resultset head +template +void boost::mysql::connection::read_resultset_head( + execution_state& st, + error_code& err, + diagnostics& diag +) +{ + detail::clear_errors(err, diag); + return detail::read_resultset_head( + get_channel(), + detail::execution_state_access::get_impl(st), + err, + diag + ); +} + +template +void boost::mysql::connection::read_resultset_head(execution_state& st) +{ + detail::error_block blk; + detail::read_resultset_head( + get_channel(), + detail::execution_state_access::get_impl(st), + blk.err, + blk.diag + ); + blk.check(BOOST_CURRENT_LOCATION); +} + +template +template +BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code)) +boost::mysql::connection::async_read_resultset_head( + execution_state& st, + diagnostics& diag, + CompletionToken&& token +) +{ + return detail::async_read_resultset_head( + get_channel(), + detail::execution_state_access::get_impl(st), + diag, + std::forward(token) + ); +} + // read some rows template boost::mysql::rows_view boost::mysql::connection::read_some_rows( diff --git a/include/boost/mysql/impl/execution_state.hpp b/include/boost/mysql/impl/execution_state.hpp index 9c0101e3..353b52bc 100644 --- a/include/boost/mysql/impl/execution_state.hpp +++ b/include/boost/mysql/impl/execution_state.hpp @@ -11,46 +11,12 @@ #pragma once #include -#include -#include #include struct boost::mysql::detail::execution_state_access { - static void reset(execution_state& st, detail::resultset_encoding encoding) noexcept - { - st.seqnum_ = 0; - st.encoding_ = encoding; - st.meta_.clear(); - st.eof_received_ = false; - } - - static void complete(execution_state& st, const detail::ok_packet& pack) - { - st.affected_rows_ = pack.affected_rows.value; - st.last_insert_id_ = pack.last_insert_id.value; - st.warnings_ = pack.warnings; - st.info_.assign(pack.info.value.begin(), pack.info.value.end()); - st.eof_received_ = true; - } - - static void prepare_meta(execution_state& st, std::size_t num_fields) { st.meta_.reserve(num_fields); } - - static void add_meta( - execution_state& st, - const detail::column_definition_packet& pack, - metadata_mode mode - ) - { - st.meta_.push_back(metadata_access::construct(pack, mode == metadata_mode::full)); - } - - static resultset_encoding get_encoding(const execution_state& st) noexcept { return st.encoding_; } - - static std::uint8_t& get_sequence_number(execution_state& st) noexcept { return st.seqnum_; } - - static std::vector& get_metadata(execution_state& st) noexcept { return st.meta_; } + static detail::execution_state_impl& get_impl(execution_state& st) noexcept { return st.impl_; } }; #endif diff --git a/include/boost/mysql/impl/results.hpp b/include/boost/mysql/impl/results.hpp index 138260a5..b0161d1a 100644 --- a/include/boost/mysql/impl/results.hpp +++ b/include/boost/mysql/impl/results.hpp @@ -8,15 +8,15 @@ #ifndef BOOST_MYSQL_IMPL_RESULTS_HPP #define BOOST_MYSQL_IMPL_RESULTS_HPP -#include #pragma once #include +#include + struct boost::mysql::detail::results_access { - static execution_state& get_state(results& result) noexcept { return result.st_; } - static rows& get_rows(results& result) noexcept { return result.rows_; } + static execution_state_impl& get_impl(results& result) noexcept { return result.impl_; } }; #endif diff --git a/include/boost/mysql/impl/resultset_view.hpp b/include/boost/mysql/impl/resultset_view.hpp new file mode 100644 index 00000000..db339b22 --- /dev/null +++ b/include/boost/mysql/impl/resultset_view.hpp @@ -0,0 +1,25 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_IMPL_RESULTSET_VIEW_HPP +#define BOOST_MYSQL_IMPL_RESULTSET_VIEW_HPP + +#pragma once + +#include + +#include + +struct boost::mysql::detail::resultset_view_access +{ + static resultset_view construct(const detail::execution_state_impl& st, std::size_t index) + { + return resultset_view(st, index); + } +}; + +#endif diff --git a/include/boost/mysql/impl/rows.hpp b/include/boost/mysql/impl/rows.hpp index 177f98ed..32bbf663 100644 --- a/include/boost/mysql/impl/rows.hpp +++ b/include/boost/mysql/impl/rows.hpp @@ -22,15 +22,7 @@ boost::mysql::rows& boost::mysql::rows::operator=(const rows_view& rhs) { - // Protect against self-assignment - if (rhs.fields_ == fields_.data()) - { - assert(rhs.num_fields_ == fields_.size()); - } - else - { - assign(rhs.fields_, rhs.num_fields_); - } + impl_.assign(rhs.fields_, rhs.num_fields_); num_columns_ = rhs.num_columns_; return *this; } @@ -41,18 +33,13 @@ boost::mysql::row_view boost::mysql::rows::at(std::size_t i) const { throw std::out_of_range("rows::at"); } - return detail::row_slice(fields_.data(), num_columns_, i); + return detail::row_slice(impl_.fields().data(), num_columns_, i); } boost::mysql::row_view boost::mysql::rows::operator[](std::size_t i) const noexcept { assert(i < size()); - return detail::row_slice(fields_.data(), num_columns_, i); + return detail::row_slice(impl_.fields().data(), num_columns_, i); } -struct boost::mysql::detail::rows_access -{ - static void clear(rows& r) noexcept { r.clear(); } -}; - #endif diff --git a/include/boost/mysql/results.hpp b/include/boost/mysql/results.hpp index cd2b3fa8..2f692190 100644 --- a/include/boost/mysql/results.hpp +++ b/include/boost/mysql/results.hpp @@ -10,20 +10,28 @@ #include #include +#include +#include #include #include #include #include +#include +#include #include +#include namespace boost { namespace mysql { /** * \brief Holds the results of a SQL query. - * \details The results (rows, metadata and additional info) are held in-memory. + * \details + * This object can store the results of single and multi resultset queries. + * For the former, you use \ref meta, \ref rows, \ref affected_rows and so on. + * For the latter, this class is a random-access collection of \ref resultset objects. * \n * \par Thread safety * Distinct objects: safe. \n @@ -32,6 +40,34 @@ namespace mysql { class results { public: +#ifdef BOOST_MYSQL_DOXYGEN + /** + * \brief A random access iterator to an element. + * \details The exact type of the iterator is unspecified. + */ + using iterator = __see_below__; +#else + using iterator = detail::results_iterator; +#endif + + /// \copydoc iterator + using const_iterator = iterator; + + /// A type that can hold elements in this collection with value semantics. + using value_type = resultset; + + /// The reference type. + using reference = resultset_view; + + /// \copydoc reference + using const_reference = resultset_view; + + /// An unsigned integer type to represent sizes. + using size_type = std::size_t; + + /// A signed integer type used to represent differences. + using difference_type = std::ptrdiff_t; + /** * \brief Default constructor. * \details Constructs an empty results object, with `this->has_value() == false`. @@ -39,7 +75,7 @@ public: * \par Exception safety * No-throw guarantee. */ - results() = default; + results() noexcept : impl_(true) {} /** * \brief Copy constructor. @@ -54,7 +90,8 @@ public: * No-throw guarantee. * * \par Object lifetimes - * View objects obtained from `other` remain valid. + * View objects obtained from `other` using \ref rows and \ref meta remain valid. + * Any other views and iterators referencing `other` are invalidated. */ results(results&& other) = default; @@ -64,7 +101,7 @@ public: * Basic guarantee. Internal allocations may throw. * * \par Object lifetimes - * View objects referencing `*this` are invalidated. + * Views and iterators referencing `*this` are invalidated. */ results& operator=(const results& other) = default; @@ -74,7 +111,8 @@ public: * Basic guarantee. Internal allocations may throw. * * \par Object lifetimes - * View objects referencing `other` remain valid. View objects + * View objects obtained from `other` using \ref rows and \ref meta remain valid. + * Any other views and iterators referencing `other` are invalidated. Views and iterators * referencing `*this` are invalidated. */ results& operator=(results&& other) = default; @@ -90,11 +128,18 @@ public: * * \par Exception safety * No-throw guarantee. + * + * \par Complexity + * Constant. */ - bool has_value() const noexcept { return st_.complete(); } + bool has_value() const noexcept { return impl_.complete(); } /** * \brief Returns the rows retrieved by the SQL query. + * \details + * For operations returning more than one resultset, returns the rows + * for the first resultset. + * * \par Preconditions * `this->has_value() == true` * @@ -105,11 +150,14 @@ public: * This function returns a view object, with reference semantics. The returned view points into * memory owned by `*this`, and will be valid as long as `*this` or an object move-constructed * from `*this` are alive. + * + * \par Complexity + * Constant. */ rows_view rows() const noexcept { assert(has_value()); - return rows_; + return impl_.get_rows(0); } /** @@ -117,6 +165,9 @@ public: * \details * The returned collection will have as many \ref metadata objects as columns retrieved by * the SQL query, and in the same order. + * \n + * For operations returning more than one resultset, returns metadata + * for the first resultset. * * \par Preconditions * `this->has_value() == true` @@ -128,53 +179,77 @@ public: * This function returns a view object, with reference semantics. The returned view points into * memory owned by `*this`, and will be valid as long as `*this` or an object move-constructed * from `*this` are alive. + * + * \par Complexity + * Constant. */ metadata_collection_view meta() const noexcept { assert(has_value()); - return st_.meta(); + return impl_.get_meta(0); } /** * \brief Returns the number of rows affected by the executed SQL statement. + * \details + * For operations returning more than one resultset, returns the + * first resultset's affected rows. + * * \par Preconditions * `this->has_value() == true` * * \par Exception safety * No-throw guarantee. + * + * \par Complexity + * Constant. */ std::uint64_t affected_rows() const noexcept { assert(has_value()); - return st_.affected_rows(); + return impl_.get_affected_rows(0); } /** * \brief Returns the last insert ID produced by the executed SQL statement. + * \details + * For operations returning more than one resultset, returns the + * first resultset's last insert ID. + * * \par Preconditions * `this->has_value() == true` * * \par Exception safety * No-throw guarantee. + * + * \par Complexity + * Constant. */ std::uint64_t last_insert_id() const noexcept { assert(has_value()); - return st_.last_insert_id(); + return impl_.get_last_insert_id(0); } /** * \brief Returns the number of warnings produced by the executed SQL statement. + * \details + * For operations returning more than one resultset, returns the + * first resultset's warning count. + * * \par Preconditions * `this->has_value() == true` * * \par Exception safety * No-throw guarantee. + * + * \par Complexity + * Constant. */ unsigned warning_count() const noexcept { assert(has_value()); - return st_.warning_count(); + return impl_.get_warning_count(0); } /** @@ -184,6 +259,9 @@ public: * href="https://dev.mysql.com/doc/c-api/8.0/en/mysql-info.html">here. * \n * The returned string always uses ASCII encoding, regardless of the connection's character set. + * \n + * For operations returning more than one resultset, returns the + * first resultset's info. * * \par Preconditions * `this->has_value() == true` @@ -195,17 +273,206 @@ public: * This function returns a view object, with reference semantics. The returned view points into * memory owned by `*this`, and will be valid as long as `*this` or an object move-constructed * from `*this` are alive. + * + * \par Complexity + * Constant. */ string_view info() const noexcept { assert(has_value()); - return st_.info(); + return impl_.get_info(0); + } + + /** + * \brief Returns an iterator pointing to the first resultset that this object contains. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned iterator and any reference obtained from it are valid as long as + * `*this` is alive. Move operations invalidate iterators. + * + * \par Complexity + * Constant. + */ + iterator begin() const noexcept + { + assert(has_value()); + return iterator(&impl_, 0); + } + + /** + * \brief Returns an iterator pointing to one-past-the-last resultset that this object contains. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned iterator and any reference obtained from it are valid as long as + * `*this` is alive. Move operations invalidate iterators. + * + * \par Complexity + * Constant. + */ + iterator end() const noexcept + { + assert(has_value()); + return iterator(&impl_, size()); + } + + /** + * \brief Returns the i-th resultset or throws an exception. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * Strong guranatee. Throws on invalid input. + * \throws std::out_of_range `i >= this->size()` + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` is alive. Move operations invalidate references. + * + * \par Complexity + * Constant. + */ + inline resultset_view at(std::size_t i) const + { + assert(has_value()); + if (i >= size()) + throw std::out_of_range("results::at: out of range"); + return detail::resultset_view_access::construct(impl_, i); + } + + /** + * \brief Returns the i-th resultset (unchecked access). + * \par Preconditions + * `this->has_value() == true && i < this->size()` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` is alive. Move operations invalidate references. + * + * \par Complexity + * Constant. + */ + resultset_view operator[](std::size_t i) const noexcept + { + assert(has_value()); + assert(i < size()); + return detail::resultset_view_access::construct(impl_, i); + } + + /** + * \brief Returns the first resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` is alive. Move operations invalidate references. + * + * \par Complexity + * Constant. + */ + resultset_view front() const noexcept { return (*this)[0]; } + + /** + * \brief Returns the last resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` is alive. Move operations invalidate references. + * + * \par Complexity + * Constant. + */ + resultset_view back() const noexcept { return (*this)[size() - 1]; } + + /** + * \brief Returns whether the collection contains any resultset. + * \details + * This function is provided for compatibility with standard collections, + * and always returns false, since any valid `results` contains at least one resultset. + * + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + bool empty() const noexcept + { + assert(has_value()); + return false; + } + + /** + * \brief Returns the number of resultsets that this collection contains. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + std::size_t size() const noexcept + { + assert(has_value()); + return impl_.num_resultsets(); + } + + /** + * \brief Returns the output parameters of a stored procedure call. + * \details + * Relevant for `CALL` operations performed using prepared statements that + * bind placeholders to `OUT` or `INOUT` parameters. Returns a row containing a field per + * bound output parameter. + * \n + * If this operation had no output parameters (e.g. it wasn't a `CALL`), returns an empty row. + * + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` is alive. Move operations invalidate references. + * + * \par Complexity + * Linear on `this->size()`. + */ + row_view out_params() const noexcept + { + assert(has_value()); + return impl_.get_out_params(); } private: - execution_state st_; - ::boost::mysql::rows rows_; - + detail::execution_state_impl impl_; #ifndef BOOST_MYSQL_DOXYGEN friend struct detail::results_access; #endif diff --git a/include/boost/mysql/resultset.hpp b/include/boost/mysql/resultset.hpp new file mode 100644 index 00000000..81533f22 --- /dev/null +++ b/include/boost/mysql/resultset.hpp @@ -0,0 +1,320 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_RESULTSET_HPP +#define BOOST_MYSQL_RESULTSET_HPP + +#include +#include +#include +#include +#include + +#include + +namespace boost { +namespace mysql { + +/** + * \brief An owning resultset, containing metadata, rows and additional info. + * \details + * Similar to \ref results, but can only represent a single resultset (while `results` can hold + * multiple resultsets). Can be used to take ownership of a \ref resultset_view. + */ +class resultset +{ +public: + /** + * \brief Constructs an empty resultset. + * \details + * The constructed object has `this->has_value() == false`. + * + * \par Exception safety + * No-throw guarantee. + */ + resultset() = default; + + /** + * \brief Copy constructor. + * \par Exception safety + * Strong guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `other`'s. + * + * \par Complexity + * Linear on rows and metadata size for `other`. + */ + resultset(const resultset& other) = default; + + /** + * \brief Move constructor. + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * Views obtained from `other` remain valid. + * + * \par Complexity + * Constant. + */ + resultset(resultset&& other) = default; + + /** + * \brief Copy assignment. + * \par Exception safety + * Basic guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `other`'s. Views obtained from `*this` + * are invalidated. + * + * \par Complexity + * Linear on rows and metadata size for `other`. + */ + resultset& operator=(const resultset& other) = default; + + /** + * \brief Move assignment. + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * Views obtained from `*this` are invalidated. Views obtained from `other` remain valid. + * + * \par Complexity + * Constant. + */ + resultset& operator=(resultset&& other) = default; + + /** + * \brief Destructor. + */ + ~resultset() = default; + + /** + * \brief Constructs a resultset object by taking ownership of a view. + * \par Exception safety + * Strong guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `v`'s (the contents of `v` will be copied + * into `*this`). + * + * \par Complexity + * Linear on rows and metadata size for `v`. + */ + resultset(resultset_view v) { assign(v); } + + /** + * \brief Replaces the contents of `*this` with a \ref resultset_view. + * \par Exception safety + * Basic guarantee. Internal allocations may throw. + * + * \par Object lifetimes + * `*this` lifetime will be independent of `v`'s (the contents of `v` will be copied + * into `*this`). Views obtained from `*this` are invalidated. + * + * \par Complexity + * Linear on rows and metadata size for `v` and `*this`. + */ + resultset& operator=(resultset_view v) + { + assign(v); + return *this; + } + + /** + * \brief Returns whether this object contains actual data or not. + * \details + * Only returns true for default-constructed objects. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + bool has_value() const noexcept { return has_value_; } + + /** + * \brief Returns the rows that this resultset contains. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` or an object move-constructed from `*this` are alive. + * + * \par Complexity + * Constant. + */ + rows_view rows() const noexcept + { + assert(has_value_); + return rws_; + } + + /** + * \brief Returns metadata for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` or an object move-constructed from `*this` are alive. + * + * \par Complexity + * Constant. + */ + metadata_collection_view meta() const noexcept + { + assert(has_value_); + return meta_; + } + + /** + * \brief Returns the number of affected rows for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + std::uint64_t affected_rows() const noexcept + { + assert(has_value_); + return affected_rows_; + } + + /** + * \brief Returns the last insert ID for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + std::uint64_t last_insert_id() const noexcept + { + assert(has_value_); + return last_insert_id_; + } + + /** + * \brief Returns the number of warnings for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + unsigned warning_count() const noexcept + { + assert(has_value_); + return warnings_; + } + + /** + * \brief Returns additional information for this resultset. + * \details + * The format of this information is documented by MySQL here. + * \n + * The returned string always uses ASCII encoding, regardless of the connection's character set. + * + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * `*this` or an object move-constructed from `*this` are alive. + * + * \par Complexity + * Constant. + */ + string_view info() const noexcept + { + assert(has_value_); + return string_view(info_.data(), info_.size()); + } + + /** + * \brief Returns whether this resultset represents a procedure OUT params. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + bool is_out_params() const noexcept + { + assert(has_value_); + return is_out_params_; + } + +private: + bool has_value_{false}; + std::vector meta_; + ::boost::mysql::rows rws_; + std::uint64_t affected_rows_{}; + std::uint64_t last_insert_id_{}; + std::uint16_t warnings_{}; + std::vector info_; + bool is_out_params_{false}; + + void assign(resultset_view v) + { + has_value_ = v.has_value(); + if (has_value_) + { + meta_.assign(v.meta().begin(), v.meta().end()); + rws_ = v.rows(); + affected_rows_ = v.affected_rows(); + last_insert_id_ = v.last_insert_id(); + warnings_ = v.warning_count(); + info_.assign(v.info().begin(), v.info().end()); + is_out_params_ = v.is_out_params(); + } + else + { + meta_.clear(); + rws_ = ::boost::mysql::rows(); + affected_rows_ = 0; + last_insert_id_ = 0; + warnings_ = 0; + info_.clear(); + is_out_params_ = false; + } + } +}; + +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/resultset_view.hpp b/include/boost/mysql/resultset_view.hpp new file mode 100644 index 00000000..c72a6d59 --- /dev/null +++ b/include/boost/mysql/resultset_view.hpp @@ -0,0 +1,207 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_RESULTSET_VIEW_HPP +#define BOOST_MYSQL_RESULTSET_VIEW_HPP + +#include +#include + +namespace boost { +namespace mysql { + +/** + * \brief A non-owning reference to a resultset. + * \details + * A `resultset_view` points to memory owned by an external object, usually a \ref results. + * The view and any other reference type obtained from it are valid as long as the + * object they point to is alive. + */ +class resultset_view +{ +public: + /** + * \brief Constructs a view with `this->has_value() == false`. + * \par Exception safety + * No-throw guarantee. + */ + resultset_view() = default; + + /** + * \brief Returns whether this is a null view or not. + * \details + * Only returns true for default-constructed views. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + bool has_value() const noexcept { return st_ != nullptr; } + + /** + * \brief Returns the rows for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * the object that `*this` points to is alive. + * + * \par Complexity + * Constant. + */ + rows_view rows() const noexcept + { + assert(has_value()); + return st_->get_rows(index_); + } + + /** + * \brief Returns metadata for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * the object that `*this` points to is alive. + * + * \par Complexity + * Constant. + */ + metadata_collection_view meta() const noexcept + { + assert(has_value()); + return st_->get_meta(index_); + } + + /** + * \brief Returns the number of affected rows for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + std::uint64_t affected_rows() const noexcept + { + assert(has_value()); + return st_->get_affected_rows(index_); + } + + /** + * \brief Returns the last insert ID for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + std::uint64_t last_insert_id() const noexcept + { + assert(has_value()); + return st_->get_last_insert_id(index_); + } + + /** + * \brief Returns the number of warnings for this resultset. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + unsigned warning_count() const noexcept + { + assert(has_value()); + return st_->get_warning_count(index_); + } + + /** + * \brief Returns additional information for this resultset. + * \details + * The format of this information is documented by MySQL here. + * \n + * The returned string always uses ASCII encoding, regardless of the connection's character set. + * + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * The returned reference and any other references obtained from it are valid as long as + * the object that `*this` points to is alive. + * + * \par Complexity + * Constant. + */ + string_view info() const noexcept + { + assert(has_value()); + return st_->get_info(index_); + } + + /** + * \brief Returns whether this resultset represents a procedure OUT params. + * \par Preconditions + * `this->has_value() == true` + * + * \par Exception safety + * No-throw guarantee. + * + * \par Complexity + * Constant. + */ + bool is_out_params() const noexcept + { + assert(has_value()); + return st_->get_is_out_params(index_); + } + +#ifndef BOOST_MYSQL_DOXYGEN + const resultset_view* operator->() const noexcept { return this; } +#endif + +private: + const detail::execution_state_impl* st_{}; + std::size_t index_{}; + + resultset_view(const detail::execution_state_impl& st, std::size_t index) noexcept + : st_(&st), index_(index) + { + } + +#ifndef BOOST_MYSQL_DOXYGEN + friend struct detail::resultset_view_access; +#endif +}; + +} // namespace mysql +} // namespace boost + +#include + +#endif diff --git a/include/boost/mysql/row.hpp b/include/boost/mysql/row.hpp index e68f8d6a..f1527885 100644 --- a/include/boost/mysql/row.hpp +++ b/include/boost/mysql/row.hpp @@ -13,7 +13,7 @@ #include #include -#include +#include #include #include @@ -35,8 +35,10 @@ namespace mysql { * `row`'s internal storage. These views behave like references, and are valid as long as pointers, * iterators and references into the `row` remain valid. */ -class row : private detail::row_base +class row { + detail::row_impl impl_; + public: #ifdef BOOST_MYSQL_DOXYGEN /** @@ -146,7 +148,7 @@ public: * \par Complexity * Linear on `r.size()`. */ - row(row_view r) : detail::row_base(r.begin(), r.size()) {} + row(row_view r) : impl_(r.begin(), r.size()) {} /** * \brief Replaces the contents with a \ref row_view. @@ -163,42 +165,33 @@ public: */ row& operator=(row_view r) { - // Protect against self-assignment. This is valid as long as we - // don't implement sub-range operators (e.g. row_view[2:4]) - if (r.fields_ == fields_.data()) - { - assert(r.size() == fields_.size()); - } - else - { - assign(r.begin(), r.size()); - } + impl_.assign(r.begin(), r.size()); return *this; } /// \copydoc row_view::begin - const_iterator begin() const noexcept { return fields_.data(); } + const_iterator begin() const noexcept { return impl_.fields().data(); } /// \copydoc row_view::end - const_iterator end() const noexcept { return fields_.data() + fields_.size(); } + const_iterator end() const noexcept { return impl_.fields().data() + impl_.fields().size(); } /// \copydoc row_view::at - field_view at(std::size_t i) const { return fields_.at(i); } + field_view at(std::size_t i) const { return impl_.fields().at(i); } /// \copydoc row_view::operator[] - field_view operator[](std::size_t i) const noexcept { return fields_[i]; } + field_view operator[](std::size_t i) const noexcept { return impl_.fields()[i]; } /// \copydoc row_view::front - field_view front() const noexcept { return fields_.front(); } + field_view front() const noexcept { return impl_.fields().front(); } /// \copydoc row_view::back - field_view back() const noexcept { return fields_.back(); } + field_view back() const noexcept { return impl_.fields().back(); } /// \copydoc row_view::empty - bool empty() const noexcept { return fields_.empty(); } + bool empty() const noexcept { return impl_.fields().empty(); } /// \copydoc row_view::size - std::size_t size() const noexcept { return fields_.size(); } + std::size_t size() const noexcept { return impl_.fields().size(); } /** * \brief Creates a \ref row_view that references `*this`. @@ -212,7 +205,7 @@ public: * \par Complexity * Constant. */ - operator row_view() const noexcept { return row_view(fields_.data(), fields_.size()); } + operator row_view() const noexcept { return row_view(impl_.fields().data(), impl_.fields().size()); } /// \copydoc row_view::as_vector template diff --git a/include/boost/mysql/rows.hpp b/include/boost/mysql/rows.hpp index ff457256..b45e7ad2 100644 --- a/include/boost/mysql/rows.hpp +++ b/include/boost/mysql/rows.hpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include namespace boost { @@ -35,7 +35,7 @@ namespace mysql { * \n * Although owning, `rows` is read-only. It's optimized for memory re-use. */ -class rows : private detail::row_base +class rows { public: #ifdef BOOST_MYSQL_DOXYGEN @@ -151,10 +151,10 @@ public: * \par Complexity * Linear on `r.size() * r.num_columns()`. */ - rows(const rows_view& r) : detail::row_base(r.fields_, r.num_fields_), num_columns_(r.num_columns_) {} + rows(const rows_view& r) : impl_(r.fields_, r.num_fields_), num_columns_(r.num_columns_) {} /** - * \brief Replaces the contents with a \ref rows_view. + * \brief Replaces the contents of `*this` with a \ref rows_view. * \par Exception safety * Basic guarantee. Internal allocations may throw. * @@ -169,10 +169,10 @@ public: inline rows& operator=(const rows_view& r); /// \copydoc rows_view::begin - iterator begin() const noexcept { return iterator(fields_.data(), num_columns_, 0); } + iterator begin() const noexcept { return iterator(impl_.fields().data(), num_columns_, 0); } /// \copydoc rows_view::end - iterator end() const noexcept { return iterator(fields_.data(), num_columns_, size()); } + iterator end() const noexcept { return iterator(impl_.fields().data(), num_columns_, size()); } /// \copydoc rows_view::at inline row_view at(std::size_t i) const; @@ -187,10 +187,10 @@ public: row_view back() const noexcept { return (*this)[size() - 1]; } /// \copydoc rows_view::empty - bool empty() const noexcept { return fields_.empty(); } + bool empty() const noexcept { return impl_.fields().empty(); } /// \copydoc rows_view::size - std::size_t size() const noexcept { return num_columns_ == 0 ? 0 : fields_.size() / num_columns_; } + std::size_t size() const noexcept { return num_columns_ == 0 ? 0 : impl_.fields().size() / num_columns_; } /// \copydoc rows_view::num_columns std::size_t num_columns() const noexcept { return num_columns_; } @@ -207,14 +207,14 @@ public: * \par Complexity * Constant. */ - operator rows_view() const noexcept { return rows_view(fields_.data(), fields_.size(), num_columns_); } + operator rows_view() const noexcept + { + return rows_view(impl_.fields().data(), impl_.fields().size(), num_columns_); + } private: + detail::row_impl impl_; std::size_t num_columns_{}; - -#ifndef BOOST_MYSQL_DOXYGEN - friend struct detail::rows_access; -#endif }; /** diff --git a/test/common/check_meta.hpp b/test/common/check_meta.hpp new file mode 100644 index 00000000..c45586bb --- /dev/null +++ b/test/common/check_meta.hpp @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CHECK_META_HPP +#define BOOST_MYSQL_TEST_COMMON_CHECK_META_HPP + +// This is a lighter check than integ tests' metadata_validator + +#include +#include +#include + +#include + +#include + +namespace boost { +namespace mysql { +namespace test { + +inline void check_meta(metadata_collection_view meta, const std::vector& expected_types) +{ + BOOST_TEST_REQUIRE(meta.size() == expected_types.size()); + for (std::size_t i = 0; i < meta.size(); ++i) + { + BOOST_TEST(meta[i].type() == expected_types[i]); + } +} + +inline void check_meta( + metadata_collection_view meta, + const std::vector>& expected +) +{ + BOOST_TEST_REQUIRE(meta.size() == expected.size()); + for (std::size_t i = 0; i < meta.size(); ++i) + { + BOOST_TEST(meta[i].type() == expected[i].first); + BOOST_TEST(meta[i].type() == expected[i].first); + } +} + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/create_execution_state.hpp b/test/common/create_execution_state.hpp deleted file mode 100644 index d9e3da83..00000000 --- a/test/common/create_execution_state.hpp +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_TEST_COMMON_CREATE_EXECUTION_STATE_HPP -#define BOOST_MYSQL_TEST_COMMON_CREATE_EXECUTION_STATE_HPP - -#include -#include - -#include -#include - -namespace boost { -namespace mysql { -namespace test { - -inline execution_state create_execution_state( - detail::resultset_encoding enc, - const std::vector& types, - std::uint8_t seqnum = 0 -) -{ - execution_state res; - detail::execution_state_access::reset(res, enc); - boost::mysql::detail::column_definition_packet coldef{}; - for (auto type : types) - { - coldef.type = type; - coldef.flags = 0; - coldef.decimals = 0; - detail::execution_state_access::add_meta(res, coldef, metadata_mode::minimal); - } - detail::execution_state_access::get_sequence_number(res) = seqnum; - return res; -} - -} // namespace test -} // namespace mysql -} // namespace boost - -#endif diff --git a/test/common/create_meta.hpp b/test/common/create_meta.hpp deleted file mode 100644 index 08320577..00000000 --- a/test/common/create_meta.hpp +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_TEST_COMMON_CREATE_META_HPP -#define BOOST_MYSQL_TEST_COMMON_CREATE_META_HPP - -#include -#include - -#include -#include -#include - -#include - -namespace boost { -namespace mysql { -namespace test { - -inline metadata create_meta(const detail::column_definition_packet& coldef, bool copy_strings) -{ - return detail::metadata_access::construct(coldef, copy_strings); -} - -inline metadata create_meta( - detail::protocol_field_type type, - std::uint16_t flags = 0, - std::uint8_t decimals = 0, - std::uint16_t collation = mysql_collations::utf8mb4_general_ci -) -{ - detail::column_definition_packet coldef{}; - coldef.type = type; - coldef.flags = flags; - coldef.decimals = decimals; - coldef.character_set = collation; - return create_meta(coldef, true); -} - -} // namespace test -} // namespace mysql -} // namespace boost - -#endif diff --git a/test/common/create_statement.hpp b/test/common/create_statement.hpp deleted file mode 100644 index 54c47fdb..00000000 --- a/test/common/create_statement.hpp +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_TEST_COMMON_CREATE_STATEMENT_HPP -#define BOOST_MYSQL_TEST_COMMON_CREATE_STATEMENT_HPP - -#include - -#include - -#include - -namespace boost { -namespace mysql { -namespace test { - -inline statement create_statement(std::uint16_t num_params, std::uint32_t stmt_id = 1) -{ - statement stmt; - detail::statement_access::reset( - stmt, - boost::mysql::detail::com_stmt_prepare_ok_packet{stmt_id, 2, num_params, 0} - ); - return stmt; -} - -} // namespace test -} // namespace mysql -} // namespace boost - -#endif diff --git a/test/common/create_diagnostics.hpp b/test/common/creation/create_diagnostics.hpp similarity index 83% rename from test/common/create_diagnostics.hpp rename to test/common/creation/create_diagnostics.hpp index b53371f4..9cab355d 100644 --- a/test/common/create_diagnostics.hpp +++ b/test/common/creation/create_diagnostics.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_TEST_COMMON_CREATE_DIAGNOSTICS_HPP -#define BOOST_MYSQL_TEST_COMMON_CREATE_DIAGNOSTICS_HPP +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_DIAGNOSTICS_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_DIAGNOSTICS_HPP #include #include diff --git a/test/common/creation/create_execution_state.hpp b/test/common/creation/create_execution_state.hpp new file mode 100644 index 00000000..395a6a9e --- /dev/null +++ b/test/common/creation/create_execution_state.hpp @@ -0,0 +1,158 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_EXECUTION_STATE_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_EXECUTION_STATE_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "creation/create_message_struct.hpp" + +namespace boost { +namespace mysql { +namespace test { + +struct resultset_spec +{ + std::vector types; + boost::mysql::rows r; + detail::ok_packet ok; + + bool empty() const noexcept { return types.empty(); } +}; + +// execution_state_impl +class exec_builder +{ + detail::execution_state_impl res_; + +public: + exec_builder(bool append_mode) : res_(append_mode) {} + + exec_builder& reset(std::vector* storage) + { + res_.reset(res_.encoding(), storage); + return *this; + } + + exec_builder& reset( + detail::resultset_encoding enc = detail::resultset_encoding::text, + std::vector* storage = nullptr + ) + { + res_.reset(enc, storage); + return *this; + } + + exec_builder& seqnum(std::uint8_t v) + { + res_.sequence_number() = v; + return *this; + } + + exec_builder& meta(const std::vector& types) + { + res_.on_num_meta(types.size()); + for (auto type : types) + { + res_.on_meta(create_coldef(type), metadata_mode::minimal); + } + return *this; + } + + exec_builder& rows(const boost::mysql::rows& r) + { + assert(r.num_columns() == res_.current_resultset_meta().size()); + res_.on_row_batch_start(); + for (auto rv : r) + { + field_view* storage = res_.add_row(); + for (auto f : rv) + { + *(storage++) = f; + } + } + res_.on_row_batch_finish(); + return *this; + } + + exec_builder& ok(const detail::ok_packet& pack) + { + if (res_.should_read_head()) + res_.on_head_ok_packet(pack); + else + res_.on_row_ok_packet(pack); + return *this; + } + + exec_builder& resultset(const resultset_spec& spec, bool is_last = false) + { + detail::ok_packet actual_ok = spec.ok; + if (!is_last) + { + actual_ok.status_flags |= detail::SERVER_MORE_RESULTS_EXISTS; + } + if (!spec.empty()) + { + meta(spec.types); + } + if (!spec.r.empty()) + { + rows(spec.r); + } + return ok(actual_ok); + } + + exec_builder& last_resultset(const resultset_spec& spec) { return resultset(spec, true); } + + detail::execution_state_impl build() { return std::move(res_); } + + execution_state build_state() + { + execution_state res; + detail::execution_state_access::get_impl(res) = build(); + return res; + } +}; + +inline results create_results(const std::vector& spec) +{ + exec_builder builder(true); + for (std::size_t i = 0; i < spec.size(); ++i) + { + builder.resultset(spec[i], i == spec.size() - 1); + } + results res; + detail::results_access::get_impl(res) = builder.build(); + return res; +} + +inline detail::execution_state_impl& get_impl(execution_state& st) +{ + return detail::execution_state_access::get_impl(st); +} + +inline detail::execution_state_impl& get_impl(results& r) { return detail::results_access::get_impl(r); } + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/create_message.hpp b/test/common/creation/create_message.hpp similarity index 57% rename from test/common/create_message.hpp rename to test/common/creation/create_message.hpp index bcf1a96d..685b8959 100644 --- a/test/common/create_message.hpp +++ b/test/common/creation/create_message.hpp @@ -5,11 +5,9 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_TEST_COMMON_CREATE_MESSAGE_HPP -#define BOOST_MYSQL_TEST_COMMON_CREATE_MESSAGE_HPP +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_MESSAGE_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_MESSAGE_HPP -#include -#include #include #include @@ -23,6 +21,7 @@ #include #include "buffer_concat.hpp" +#include "creation/create_message_struct.hpp" namespace boost { namespace mysql { @@ -66,105 +65,89 @@ inline std::vector create_message( ); } +template +void serialize_to_vector(std::vector& res, const Args&... args) +{ + detail::serialization_context ctx(detail::capabilities(0)); + std::size_t size = detail::get_size(ctx, args...); + res.resize(res.size() + size); + ctx.set_first(res.data()); + detail::serialize(ctx, args...); +} + template std::vector serialize_to_vector(const Args&... args) { std::vector res; - detail::serialization_context ctx(detail::capabilities(0)); - std::size_t size = detail::get_size(ctx, args...); - res.resize(size); - ctx.set_first(res.data()); - detail::serialize(ctx, args...); + serialize_to_vector(res, args...); return res; } -inline detail::ok_packet create_ok_packet( - std::uint64_t affected_rows = 0, - std::uint64_t last_insert_id = 0, - std::uint16_t status_flags = 0, - std::uint16_t warnings = 0, - string_view info = "" -) +class ok_msg_builder { - return detail::ok_packet{ - detail::int_lenenc{affected_rows}, - detail::int_lenenc{last_insert_id}, - status_flags, - warnings, - detail::string_lenenc{info}, - }; -} + ok_builder impl_; + std::uint8_t seqnum_{}; -inline std::vector create_ok_packet_body( - std::uint64_t affected_rows = 0, - std::uint64_t last_insert_id = 0, - std::uint16_t status_flags = 0, - std::uint16_t warnings = 0, - string_view info = "", - std::uint8_t header = 0x00 -) -{ - auto pack = create_ok_packet(affected_rows, last_insert_id, status_flags, warnings, info); - auto res = serialize_to_vector( - std::uint8_t(header), - pack.affected_rows, - pack.last_insert_id, - pack.status_flags, - pack.warnings - ); - // When info is empty, it's actually omitted in the ok_packet - if (!info.empty()) +public: + ok_msg_builder() = default; + ok_msg_builder seqnum(std::uint8_t v) { - auto vinfo = serialize_to_vector(pack.info); - concat(res, vinfo); + seqnum_ = v; + return *this; + } + ok_msg_builder& affected_rows(std::uint64_t v) + { + impl_.affected_rows(v); + return *this; + } + ok_msg_builder& last_insert_id(std::uint64_t v) + { + impl_.last_insert_id(v); + return *this; + } + ok_msg_builder& warnings(std::uint16_t v) + { + impl_.warnings(v); + return *this; + } + ok_msg_builder& info(string_view v) + { + impl_.info(v); + return *this; + } + ok_msg_builder& more_results(bool v) + { + impl_.more_results(v); + return *this; } - return res; -} -inline std::vector create_ok_packet_message( - std::uint8_t seqnum, - std::uint64_t affected_rows = 0, - std::uint64_t last_insert_id = 0, - std::uint16_t status_flags = 0, - std::uint16_t warnings = 0, - string_view info = "", - std::uint8_t header = 0x00 -) -{ - return create_message( - seqnum, - create_ok_packet_body(affected_rows, last_insert_id, status_flags, warnings, info, header) - ); -} + std::vector build_body(std::uint8_t header = 0x00) + { + auto pack = impl_.build(); + auto res = serialize_to_vector( + std::uint8_t(header), + pack.affected_rows, + pack.last_insert_id, + pack.status_flags, + pack.warnings + ); + // When info is empty, it's actually omitted in the ok_packet + if (!pack.info.value.empty()) + { + auto vinfo = serialize_to_vector(pack.info); + concat(res, vinfo); + } + return res; + } -inline std::vector create_eof_packet_message( - std::uint8_t seqnum, - std::uint64_t affected_rows = 0, - std::uint64_t last_insert_id = 0, - std::uint16_t status_flags = 0, - std::uint16_t warnings = 0, - string_view info = "" -) -{ - return create_ok_packet_message( - seqnum, - affected_rows, - last_insert_id, - status_flags, - warnings, - info, - 0xfe - ); -} + std::vector build_ok() { return create_message(seqnum_, build_body(0)); } + + std::vector build_eof() { return create_message(seqnum_, build_body(0xfe)); } +}; inline std::vector create_err_packet_body(std::uint16_t code, string_view message = "") { - detail::err_packet pack{ - code, - detail::string_fixed<1>{}, - detail::string_fixed<5>{}, - detail::string_eof{message}, - }; + auto pack = create_err_packet(code, message); return serialize_to_vector( std::uint8_t(0xff), pack.error_code, @@ -194,19 +177,7 @@ inline std::vector create_coldef_message( string_view name = "mycol" ) { - boost::mysql::detail::column_definition_packet pack{ - detail::string_lenenc("def"), - detail::string_lenenc("mydb"), - detail::string_lenenc("mytable"), - detail::string_lenenc("mytable"), - detail::string_lenenc(name), - detail::string_lenenc(name), - mysql_collations::utf8_general_ci, - 10, // column_length - type, - 0, // flags - 0, // decimals - }; + auto pack = create_coldef(type, name); return create_message( seqnum, serialize_to_vector( diff --git a/test/common/creation/create_message_struct.hpp b/test/common/creation/create_message_struct.hpp new file mode 100644 index 00000000..ed3b2f69 --- /dev/null +++ b/test/common/creation/create_message_struct.hpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_MESSAGE_STRUCT_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_MESSAGE_STRUCT_HPP + +#include + +#include +#include + +namespace boost { +namespace mysql { +namespace test { + +class ok_builder +{ + detail::ok_packet pack_{}; + + void flag(std::uint16_t f, bool value) + { + if (value) + pack_.status_flags |= f; + else + pack_.status_flags &= ~f; + } + +public: + ok_builder() = default; + ok_builder& affected_rows(std::uint64_t v) + { + pack_.affected_rows.value = v; + return *this; + } + ok_builder& last_insert_id(std::uint64_t v) + { + pack_.last_insert_id.value = v; + return *this; + } + ok_builder& warnings(std::uint16_t v) + { + pack_.warnings = v; + return *this; + } + ok_builder& more_results(bool v = true) + { + flag(detail::SERVER_MORE_RESULTS_EXISTS, v); + return *this; + } + ok_builder& out_params(bool v = true) + { + flag(detail::SERVER_PS_OUT_PARAMS, v); + return *this; + } + ok_builder& info(string_view v) + { + pack_.info.value = v; + return *this; + } + detail::ok_packet build() { return pack_; } +}; + +inline detail::err_packet create_err_packet(std::uint16_t code, string_view message = "") +{ + return detail::err_packet{ + code, + detail::string_fixed<1>{}, + detail::string_fixed<5>{}, + detail::string_eof{message}, + }; +} + +inline detail::column_definition_packet create_coldef( + detail::protocol_field_type type, + string_view name = "mycol" +) +{ + return boost::mysql::detail::column_definition_packet{ + detail::string_lenenc("def"), + detail::string_lenenc("mydb"), + detail::string_lenenc("mytable"), + detail::string_lenenc("mytable"), + detail::string_lenenc(name), + detail::string_lenenc(name), + 33, // utf8_general_ci + 10, // column_length + type, + 0, // flags + 0, // decimals + }; +} + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/creation/create_meta.hpp b/test/common/creation/create_meta.hpp new file mode 100644 index 00000000..33a3b6d0 --- /dev/null +++ b/test/common/creation/create_meta.hpp @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_META_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_META_HPP + +#include +#include + +#include + +namespace boost { +namespace mysql { +namespace test { + +class meta_builder +{ + detail::column_definition_packet pack_{}; + +public: + meta_builder(detail::protocol_field_type t) + { + pack_.type = t; + pack_.character_set = 33; // utf8_general_ci + } + meta_builder& flags(std::uint16_t v) noexcept + { + pack_.flags = v; + return *this; + } + meta_builder& decimals(std::uint8_t v) noexcept + { + pack_.decimals = v; + return *this; + } + meta_builder& collation(std::uint16_t v) noexcept + { + pack_.character_set = v; + return *this; + } + metadata build() { return detail::metadata_access::construct(pack_, false); } +}; + +inline metadata create_meta(detail::protocol_field_type t) { return meta_builder(t).build(); } + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/creation/create_row_message.hpp b/test/common/creation/create_row_message.hpp new file mode 100644 index 00000000..8ea713bc --- /dev/null +++ b/test/common/creation/create_row_message.hpp @@ -0,0 +1,55 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_ROW_MESSAGE_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_ROW_MESSAGE_HPP + +#include + +#include + +#include +#include + +#include "creation/create_message.hpp" +#include "test_common.hpp" + +namespace boost { +namespace mysql { +namespace test { + +template +std::vector create_text_row_message(std::uint8_t seqnum, const Args&... args) +{ + auto fields = make_fv_arr(args...); + std::vector res; + for (field_view f : fields) + { + if (f.is_int64() || f.is_uint64()) + { + auto s = f.is_int64() ? std::to_string(f.get_int64()) : std::to_string(f.get_uint64()); + detail::string_lenenc slenenc{s}; + serialize_to_vector(res, slenenc); + } + else if (f.is_string()) + { + detail::string_lenenc slenenc{f.get_string()}; + serialize_to_vector(res, slenenc); + } + else + { + throw std::runtime_error("create_text_row_message: type not implemented"); + } + } + return create_message(seqnum, std::move(res)); +} + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/creation/create_statement.hpp b/test/common/creation/create_statement.hpp new file mode 100644 index 00000000..16bd12b1 --- /dev/null +++ b/test/common/creation/create_statement.hpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_STATEMENT_HPP +#define BOOST_MYSQL_TEST_COMMON_CREATION_CREATE_STATEMENT_HPP + +#include + +#include + +#include + +namespace boost { +namespace mysql { +namespace test { + +class statement_builder +{ + boost::mysql::detail::com_stmt_prepare_ok_packet pack_{}; + +public: + statement_builder() = default; + statement_builder& id(std::uint32_t v) noexcept + { + pack_.statement_id = v; + return *this; + } + statement_builder& num_params(std::uint16_t v) noexcept + { + pack_.num_params = v; + return *this; + } + statement build() + { + statement stmt; + detail::statement_access::reset(stmt, pack_); + return stmt; + } +}; + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/common/impl/test_stream.hpp b/test/common/impl/test_stream.hpp index 8a890d98..4b6e604b 100644 --- a/test/common/impl/test_stream.hpp +++ b/test/common/impl/test_stream.hpp @@ -24,8 +24,7 @@ #include "../test_stream.hpp" #include "buffer_concat.hpp" -boost::mysql::test::test_stream::test_stream(fail_count fc, executor_type ex) - : fail_count_(fc), executor_(ex) +boost::mysql::test::test_stream::test_stream(fail_count fc, executor_type ex) : fail_count_(fc), executor_(ex) { } @@ -46,7 +45,7 @@ boost::mysql::test::test_stream::test_stream(read_behavior b, fail_count fc, exe { } -void boost::mysql::test::test_stream::add_message( +boost::mysql::test::test_stream& boost::mysql::test::test_stream::add_message( const std::vector& bytes, bool separate_reads ) @@ -56,12 +55,14 @@ void boost::mysql::test::test_stream::add_message( read_break_offsets_.insert(bytes_to_read_.size()); } concat(bytes_to_read_, bytes); + return *this; } -void boost::mysql::test::test_stream::set_read_behavior(read_behavior b) +boost::mysql::test::test_stream& boost::mysql::test::test_stream::set_read_behavior(read_behavior b) { bytes_to_read_ = std::move(b.bytes_to_read); read_break_offsets_ = std::move(b.break_offsets); + return *this; } template @@ -70,8 +71,7 @@ struct boost::mysql::test::test_stream::read_op : boost::asio::coroutine test_stream& stream_; MutableBufferSequence buffers_; - read_op(test_stream& stream, const MutableBufferSequence& buffers) - : stream_(stream), buffers_(buffers){}; + read_op(test_stream& stream, const MutableBufferSequence& buffers) : stream_(stream), buffers_(buffers){}; template void operator()(Self& self) @@ -110,8 +110,7 @@ struct boost::mysql::test::test_stream::write_op : boost::asio::coroutine test_stream& stream_; ConstBufferSequence buffers_; - write_op(test_stream& stream, const ConstBufferSequence& buffers) - : stream_(stream), buffers_(buffers){}; + write_op(test_stream& stream, const ConstBufferSequence& buffers) : stream_(stream), buffers_(buffers){}; template void operator()(Self& self) @@ -132,10 +131,7 @@ template < class ConstBufferSequence, BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, std::size_t)) CompletionToken> BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(boost::mysql::error_code, std::size_t)) -boost::mysql::test::test_stream::async_write_some( - const ConstBufferSequence& buffers, - CompletionToken&& token -) +boost::mysql::test::test_stream::async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token) { return boost::asio::async_compose( write_op(*this, buffers), @@ -153,10 +149,7 @@ std::size_t boost::mysql::test::test_stream::get_size_to_read(std::size_t buffer } template -std::size_t boost::mysql::test::test_stream::do_read( - const MutableBufferSequence& buffers, - error_code& ec -) +std::size_t boost::mysql::test::test_stream::do_read(const MutableBufferSequence& buffers, error_code& ec) { // Fail count error_code err = fail_count_.maybe_fail(); @@ -200,10 +193,7 @@ std::size_t boost::mysql::test::test_stream::do_read( } template -std::size_t boost::mysql::test::test_stream::do_write( - const ConstBufferSequence& buffers, - error_code& ec -) +std::size_t boost::mysql::test::test_stream::do_write(const ConstBufferSequence& buffers, error_code& ec) { // Fail count error_code err = fail_count_.maybe_fail(); @@ -220,8 +210,7 @@ std::size_t boost::mysql::test::test_stream::do_write( for (auto it = first; it != last && num_bytes_written < write_break_size_; ++it) { boost::asio::const_buffer buff = *it; - std::size_t num_bytes_to_transfer = (std::min - )(buff.size(), write_break_size_ - num_bytes_written); + std::size_t num_bytes_to_transfer = (std::min)(buff.size(), write_break_size_ - num_bytes_written); concat(bytes_written_, buff.data(), num_bytes_to_transfer); num_bytes_written += num_bytes_to_transfer; } diff --git a/test/common/netfun_helpers.hpp b/test/common/netfun_helpers.hpp index 96b41ab8..aa41ea3a 100644 --- a/test/common/netfun_helpers.hpp +++ b/test/common/netfun_helpers.hpp @@ -21,7 +21,7 @@ #include #include -#include "create_diagnostics.hpp" +#include "creation/create_diagnostics.hpp" #include "network_result.hpp" // Helper functions and classes to implement netmakers diff --git a/test/common/printing.hpp b/test/common/printing.hpp index 704ee1d1..2525e278 100644 --- a/test/common/printing.hpp +++ b/test/common/printing.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -129,6 +130,11 @@ inline std::ostream& operator<<(std::ostream& os, deserialize_errc v) } } +inline std::ostream& operator<<(std::ostream& os, results_iterator it) +{ + return os << "results_iterator(" << static_cast(it.obj()) << ", index=" << it.index() << ")"; +} + } // namespace detail } // namespace mysql } // namespace boost diff --git a/test/common/test_channel.hpp b/test/common/test_channel.hpp index 70999a35..2d79c18a 100644 --- a/test/common/test_channel.hpp +++ b/test/common/test_channel.hpp @@ -20,10 +20,7 @@ namespace test { using test_channel = detail::channel; -inline test_channel create_channel( - std::vector messages = {}, - std::size_t buffer_size = 0 -) +inline test_channel create_channel(std::vector messages = {}, std::size_t buffer_size = 1024) { return test_channel(buffer_size, std::move(messages)); } diff --git a/test/common/test_connection.hpp b/test/common/test_connection.hpp index 8f2af3f4..a357f224 100644 --- a/test/common/test_connection.hpp +++ b/test/common/test_connection.hpp @@ -10,6 +10,7 @@ #include +#include "test_channel.hpp" #include "test_stream.hpp" namespace boost { @@ -18,6 +19,11 @@ namespace test { using test_connection = connection; +inline test_channel& get_channel(test_connection& conn) noexcept +{ + return boost::mysql::detail::connection_access::get_channel(conn); +} + } // namespace test } // namespace mysql } // namespace boost diff --git a/test/common/test_stream.hpp b/test/common/test_stream.hpp index 1a27cd08..a0574485 100644 --- a/test/common/test_stream.hpp +++ b/test/common/test_stream.hpp @@ -67,10 +67,7 @@ public: }; // Constructors - inline test_stream( - fail_count fc = fail_count(), - executor_type ex = boost::asio::system_executor() - ); + inline test_stream(fail_count fc = fail_count(), executor_type ex = boost::asio::system_executor()); inline test_stream( std::vector bytes_to_read, @@ -85,18 +82,27 @@ public: ); // Setting test behavior - inline void add_message(const std::vector& bytes, bool separate_reads = true); - inline void set_read_behavior(read_behavior b); - void set_write_break_size(std::size_t size) noexcept { write_break_size_ = size; } - void set_fail_count(const fail_count& fc) noexcept { fail_count_ = fc; } - void set_executor(executor_type ex) { executor_ = ex; } + inline test_stream& add_message(const std::vector& bytes, bool separate_reads = true); + inline test_stream& set_read_behavior(read_behavior b); + test_stream& set_write_break_size(std::size_t size) noexcept + { + write_break_size_ = size; + return *this; + } + test_stream& set_fail_count(const fail_count& fc) noexcept + { + fail_count_ = fc; + return *this; + } + test_stream& set_executor(executor_type ex) + { + executor_ = ex; + return *this; + } // Getting test results std::size_t num_bytes_read() const noexcept { return num_bytes_read_; } - std::size_t num_unread_bytes() const noexcept - { - return bytes_to_read_.size() - num_bytes_read_; - } + std::size_t num_unread_bytes() const noexcept { return bytes_to_read_.size() - num_bytes_read_; } const std::vector& bytes_written() const noexcept { return bytes_written_; } // Stream operations @@ -114,15 +120,13 @@ public: template < class MutableBufferSequence, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, std::size_t)) - CompletionToken> + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, std::size_t)) CompletionToken> BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code, std::size_t)) async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token); template < class ConstBufferSequence, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, std::size_t)) - CompletionToken> + BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, std::size_t)) CompletionToken> BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void(error_code, std::size_t)) async_write_some(ConstBufferSequence const& buffers, CompletionToken&& token); diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index 96127bd8..2d322c17 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -29,6 +29,8 @@ add_executable( crud.cpp handshake.cpp prepared_statements.cpp + stored_procedures.cpp + multi_queries.cpp reconnect.cpp db_specific.cpp database_types.cpp diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 1a17edd6..b7c7da15 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -66,6 +66,8 @@ unit-test boost_mysql_integrationtests crud.cpp handshake.cpp prepared_statements.cpp + stored_procedures.cpp + multi_queries.cpp reconnect.cpp db_specific.cpp database_types.cpp diff --git a/test/integration/crud.cpp b/test/integration/crud.cpp index f1c1b36d..86913512 100644 --- a/test/integration/crud.cpp +++ b/test/integration/crud.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -33,6 +34,7 @@ BOOST_FIXTURE_TEST_CASE(query_empty_select, tcp_network_fixture) conn.query("SELECT * FROM empty_table", result); // Verify results + BOOST_TEST(result.size() == 1u); validate_2fields_meta(result.meta(), "empty_table"); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 0u); @@ -41,6 +43,25 @@ BOOST_FIXTURE_TEST_CASE(query_empty_select, tcp_network_fixture) BOOST_TEST(result.info() == ""); } +BOOST_FIXTURE_TEST_CASE(query_empty_select_multifn, tcp_network_fixture) +{ + connect(); + + // Issue query + boost::mysql::execution_state st; + conn.start_query("SELECT * FROM empty_table", st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + validate_2fields_meta(st.meta(), "empty_table"); + + // Read eof + auto rv = conn.read_some_rows(st); + BOOST_TEST(rv.empty()); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); +} + BOOST_FIXTURE_TEST_CASE(query_insert, tcp_network_fixture) { connect(); @@ -51,6 +72,7 @@ BOOST_FIXTURE_TEST_CASE(query_insert, tcp_network_fixture) conn.query("INSERT INTO inserts_table (field_varchar, field_date) VALUES ('v0', '2010-10-11')", result); // Verify results + BOOST_TEST(result.size() == 1u); BOOST_TEST(result.meta().empty()); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 1u); @@ -73,6 +95,7 @@ BOOST_FIXTURE_TEST_CASE(query_update, tcp_network_fixture) conn.query("UPDATE updates_table SET field_int = field_int+10", result); // Validate results + BOOST_TEST(result.size() == 1u); BOOST_TEST(result.meta().empty()); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 2u); // there are 3 rows, but 1 has field_int = NULL @@ -95,6 +118,7 @@ BOOST_FIXTURE_TEST_CASE(query_delete, tcp_network_fixture) conn.query("DELETE FROM updates_table", result); // Validate results + BOOST_TEST(result.size() == 1u); BOOST_TEST(result.meta().empty()); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 3u); @@ -119,6 +143,7 @@ BOOST_FIXTURE_TEST_CASE(statement_update, tcp_network_fixture) // Execute it results result; conn.execute_statement(stmt, std::make_tuple(200, "f0"), result); + BOOST_TEST(result.size() == 1u); BOOST_TEST(result.meta().empty()); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 1u); @@ -146,6 +171,7 @@ BOOST_FIXTURE_TEST_CASE(statement_delete, tcp_network_fixture) // Execute it results result; conn.execute_statement(stmt, std::make_tuple("f0"), result); + BOOST_TEST(result.size() == 1u); BOOST_TEST(result.meta().empty()); BOOST_TEST(result.rows().empty()); BOOST_TEST(result.affected_rows() == 1u); diff --git a/test/integration/db_setup.sql b/test/integration/db_setup.sql index deebbc77..c6ab4715 100644 --- a/test/integration/db_setup.sql +++ b/test/integration/db_setup.sql @@ -484,5 +484,42 @@ CREATE USER 'mysqlnp_empty_password_user'@'%' IDENTIFIED WITH 'mysql_native_pass ALTER USER 'mysqlnp_empty_password_user'@'%' IDENTIFIED BY ''; GRANT ALL PRIVILEGES ON boost_mysql_integtests.* TO 'mysqlnp_empty_password_user'@'%'; +-- Stored procedures +DELIMITER // + +CREATE PROCEDURE sp_insert(IN pin VARCHAR(255)) +BEGIN + INSERT INTO inserts_table (field_varchar) VALUES (pin); +END // + +CREATE PROCEDURE sp_select_1(IN pin VARCHAR(255)) +BEGIN + SELECT * FROM one_row_table; +END // + +CREATE PROCEDURE sp_select_2(IN pin1 VARCHAR(255), IN pin2 INT) +BEGIN + SELECT * FROM one_row_table; + SELECT pin1, pin2; +END // + +CREATE PROCEDURE sp_outparams( + IN pin INT, + OUT pout INT, + INOUT pinout INT +) +BEGIN + SELECT * FROM one_row_table; + SET pout = pin; + SET pinout = pinout + 1; +END // + +CREATE PROCEDURE sp_signal() +BEGIN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'An error occurred', MYSQL_ERRNO = 1002; +END // + +DELIMITER ; + COMMIT; FLUSH PRIVILEGES; \ No newline at end of file diff --git a/test/integration/multi_queries.cpp b/test/integration/multi_queries.cpp new file mode 100644 index 00000000..e2d286b4 --- /dev/null +++ b/test/integration/multi_queries.cpp @@ -0,0 +1,130 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include + +#include "integration_test_common.hpp" +#include "printing.hpp" +#include "tcp_network_fixture.hpp" +#include "test_common.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_multi_queries) + +BOOST_FIXTURE_TEST_CASE(empty_results, tcp_network_fixture) +{ + params.set_multi_queries(true); + connect(); + start_transaction(); + + results result; + conn.query( + R"%( + INSERT INTO inserts_table (field_varchar) VALUES ('abc'); + INSERT INTO inserts_table (field_varchar) VALUES ('def'); + DELETE FROM updates_table + )%", + result + ); + + // Validate results + BOOST_TEST_REQUIRE(result.size() == 3u); + + BOOST_TEST(result[0].meta().size() == 0u); + BOOST_TEST(result[0].rows() == rows()); + BOOST_TEST(result[0].affected_rows() == 1u); + BOOST_TEST(result[0].warning_count() == 0u); + BOOST_TEST(result[0].last_insert_id() > 0u); + BOOST_TEST(result[0].info() == ""); + BOOST_TEST(!result[0].is_out_params()); + + BOOST_TEST(result[1].meta().size() == 0u); + BOOST_TEST(result[1].rows() == rows()); + BOOST_TEST(result[1].affected_rows() == 1u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() > 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(!result[1].is_out_params()); + + BOOST_TEST(result[2].meta().size() == 0u); + BOOST_TEST(result[2].rows() == rows()); + BOOST_TEST(result[2].affected_rows() == 3u); + BOOST_TEST(result[2].warning_count() == 0u); + BOOST_TEST(result[2].last_insert_id() == 0u); + BOOST_TEST(result[2].info() == ""); + BOOST_TEST(!result[2].is_out_params()); +} + +BOOST_FIXTURE_TEST_CASE(data_results, tcp_network_fixture) +{ + params.set_multi_queries(true); + connect(); + start_transaction(); + + results result; + conn.query( + R"%( + SELECT * FROM one_row_table; + SELECT * FROM empty_table; + DELETE FROM updates_table + )%", + result + ); + + // Validate results + BOOST_TEST_REQUIRE(result.size() == 3u); + + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(result[0].affected_rows() == 0u); + BOOST_TEST(result[0].warning_count() == 0u); + BOOST_TEST(result[0].last_insert_id() == 0u); + BOOST_TEST(result[0].info() == ""); + BOOST_TEST(!result[0].is_out_params()); + + validate_2fields_meta(result[1].meta(), "empty_table"); + BOOST_TEST(result[1].rows() == makerows(2)); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(!result[1].is_out_params()); + + BOOST_TEST(result[2].meta().size() == 0u); + BOOST_TEST(result[2].rows() == rows()); + BOOST_TEST(result[2].affected_rows() == 3u); + BOOST_TEST(result[2].warning_count() == 0u); + BOOST_TEST(result[2].last_insert_id() == 0u); + BOOST_TEST(result[2].info() == ""); + BOOST_TEST(!result[2].is_out_params()); +} + +BOOST_FIXTURE_TEST_CASE(error_not_enabled, tcp_network_fixture) +{ + connect(); + + results result; + error_code err; + diagnostics diag; + + conn.query("SELECT 1; SELECT 2", result, err, diag); + BOOST_TEST(err == common_server_errc::er_parse_error); + validate_string_contains(diag.server_message(), {"you have an error in your sql syntax"}); +} + +BOOST_AUTO_TEST_SUITE_END() // test_crud + +} // namespace diff --git a/test/integration/prepared_statements.cpp b/test/integration/prepared_statements.cpp index 3703522b..139e61fc 100644 --- a/test/integration/prepared_statements.cpp +++ b/test/integration/prepared_statements.cpp @@ -125,13 +125,13 @@ BOOST_FIXTURE_TEST_CASE(multifn, tcp_network_fixture) // Execute it execution_state st; conn.start_statement_execution(stmt, std::make_tuple(), st); - BOOST_TEST_REQUIRE(!st.complete()); + BOOST_TEST_REQUIRE(st.should_read_rows()); // We don't know how many rows there will be in each batch, // but they will come in order std::size_t call_count = 0; std::vector all_rows; - while (!st.complete() && call_count <= 4) + while (st.should_read_rows() && call_count <= 4) { ++call_count; for (row_view rv : conn.read_some_rows(st)) diff --git a/test/integration/spotchecks.cpp b/test/integration/spotchecks.cpp index c03f3425..d3439d41 100644 --- a/test/integration/spotchecks.cpp +++ b/test/integration/spotchecks.cpp @@ -5,6 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -71,7 +72,7 @@ BOOST_MYSQL_NETWORK_TEST(start_query_success, network_fixture, all_network_sampl execution_state st; conn->start_query("SELECT * FROM empty_table", st).get(); - BOOST_TEST(!st.complete()); + BOOST_TEST(st.should_read_rows()); validate_2fields_meta(st.meta(), "empty_table"); } @@ -135,7 +136,7 @@ BOOST_MYSQL_NETWORK_TEST(start_statement_execution_it_success, network_fixture, std::forward_list params{field_view("item"), field_view(42)}; conn->start_statement_execution(stmt, params.begin(), params.end(), st).validate_no_error(); validate_2fields_meta(st.meta(), "empty_table"); - BOOST_TEST(!st.complete()); + BOOST_TEST(st.should_read_rows()); } BOOST_MYSQL_NETWORK_TEST(start_statement_execution_it_error, network_fixture, err_net_samples) @@ -169,7 +170,7 @@ BOOST_MYSQL_NETWORK_TEST(start_statement_execution_tuple_success, network_fixtur execution_state st; conn->start_statement_execution(stmt, field_view(42), field_view(40), st).validate_no_error(); validate_2fields_meta(st.meta(), "empty_table"); - BOOST_TEST(!st.complete()); + BOOST_TEST(st.should_read_rows()); } BOOST_MYSQL_NETWORK_TEST(start_statement_execution_tuple_error, network_fixture, err_net_samples) @@ -246,7 +247,7 @@ BOOST_MYSQL_NETWORK_TEST(read_some_rows_success, network_fixture, all_network_sa // Generate an execution state execution_state st; conn->start_query("SELECT * FROM one_row_table", st); - BOOST_TEST_REQUIRE(!st.complete()); + BOOST_TEST_REQUIRE(st.should_read_rows()); // Read once. st may or may not be complete, depending // on how the buffer reallocated memory @@ -264,6 +265,55 @@ BOOST_MYSQL_NETWORK_TEST(read_some_rows_success, network_fixture, all_network_sa validate_eof(st); } +// Read resultset head +BOOST_MYSQL_NETWORK_TEST(read_resultset_head_success, network_fixture, all_network_samples()) +{ + params.set_multi_queries(true); + setup_and_connect(sample.net); + + // Generate an execution state + execution_state st; + conn->start_query("SELECT * FROM empty_table; SELECT * FROM one_row_table", st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + + // Read the OK packet to finish 1st resultset + conn->read_some_rows(st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_head()); + + // Read head + conn->read_resultset_head(st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + + // Reading head again does nothing + conn->read_resultset_head(st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + + // We can read rows now + auto rows = conn->read_some_rows(st).get(); + BOOST_TEST((rows == makerows(2, 1, "f0"))); +} + +BOOST_MYSQL_NETWORK_TEST(read_resultset_head_error, network_fixture, all_network_samples()) +{ + params.set_multi_queries(true); + setup_and_connect(sample.net); + + // Generate an execution state + execution_state st; + conn->start_query("SELECT * FROM empty_table; SELECT bad_field FROM one_row_table", st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + + // Read the OK packet to finish 1st resultset + conn->read_some_rows(st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_head()); + + // Read head for the 2nd resultset. This one contains an error, which is detected when reading head. + conn->read_resultset_head(st).validate_error_exact( + common_server_errc::er_bad_field_error, + "Unknown column 'bad_field' in 'field list'" + ); +} + // Ping BOOST_MYSQL_NETWORK_TEST(ping_success, network_fixture, all_network_samples()) { diff --git a/test/integration/stored_procedures.cpp b/test/integration/stored_procedures.cpp new file mode 100644 index 00000000..0176723c --- /dev/null +++ b/test/integration/stored_procedures.cpp @@ -0,0 +1,310 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include + +#include + +#include "check_meta.hpp" +#include "integration_test_common.hpp" +#include "printing.hpp" +#include "tcp_network_fixture.hpp" +#include "test_common.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_stored_procedures) + +BOOST_FIXTURE_TEST_CASE(without_selects, tcp_network_fixture) +{ + connect(); + start_transaction(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_insert(?)"); + + // Call the procedure + results result; + conn.execute_statement(stmt, std::make_tuple("abc"), result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 1u); + BOOST_TEST(result.meta().size() == 0u); + BOOST_TEST(result.rows().empty()); + BOOST_TEST(result.affected_rows() == 1u); + BOOST_TEST(result.warning_count() == 0u); + BOOST_TEST(result.last_insert_id() == 0u); // this refers to the CALL, not to the INSERT + BOOST_TEST(result.info() == ""); + BOOST_TEST(result.out_params() == row_view()); + + // Verify it took place + conn.query("SELECT field_varchar FROM inserts_table", result); + BOOST_TEST(result.rows().at(0).at(0).as_string() == "abc"); +} + +BOOST_FIXTURE_TEST_CASE(with_one_select, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_select_1(?)"); + + // Call the procedure + results result; + conn.execute_statement(stmt, std::make_tuple("abc"), result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 2u); + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(result[0].affected_rows() == 0u); + BOOST_TEST(result[0].warning_count() == 0u); + BOOST_TEST(result[0].last_insert_id() == 0u); + BOOST_TEST(result[0].info() == ""); + BOOST_TEST(result[1].meta().size() == 0u); + BOOST_TEST(result[1].rows().size() == 0u); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(result.out_params() == row_view()); +} + +BOOST_FIXTURE_TEST_CASE(with_two_selects, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_select_2(?, ?)"); + + // Call the procedure + results result; + conn.execute_statement(stmt, std::make_tuple("abc", 42), result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 3u); + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(result[0].affected_rows() == 0u); + BOOST_TEST(result[0].warning_count() == 0u); + BOOST_TEST(result[0].last_insert_id() == 0u); + BOOST_TEST(result[0].info() == ""); + check_meta(result[1].meta(), {column_type::varchar, column_type::int_}); + BOOST_TEST(result[1].rows() == makerows(2, "abc", 42)); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(result[2].meta().size() == 0u); + BOOST_TEST(result[2].rows().size() == 0u); + BOOST_TEST(result[2].affected_rows() == 0u); + BOOST_TEST(result[2].warning_count() == 0u); + BOOST_TEST(result[2].last_insert_id() == 0u); + BOOST_TEST(result[2].info() == ""); + BOOST_TEST(result.out_params() == row_view()); +} + +BOOST_FIXTURE_TEST_CASE(with_two_selects_multifn, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_select_2(?, ?)"); + + // Call the procedure + execution_state st; + conn.start_statement_execution(stmt, std::make_tuple("abc", 42), st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + validate_2fields_meta(st.meta(), "one_row_table"); + + // Read rows for the 1st select + auto rv = conn.read_some_rows(st); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(rv == makerows(2, 1, "f0")); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); + BOOST_TEST(!st.is_out_params()); + + // Read 2nd resultset's head + conn.read_resultset_head(st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::varchar, column_type::int_}); + + // Read 2nd resultset's rows + rv = conn.read_some_rows(st); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(rv == makerows(2, "abc", 42)); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); + BOOST_TEST(!st.is_out_params()); + + // Read final resultset + conn.read_resultset_head(st); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); + BOOST_TEST(!st.is_out_params()); +} + +BOOST_FIXTURE_TEST_CASE(output_params_not_bound, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_outparams(?, @var1, @var2)"); + + // Call the procedure + results result; + conn.execute_statement(stmt, std::make_tuple(10), result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 2u); + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(result[1].meta().size() == 0u); + BOOST_TEST(result[1].rows().size() == 0u); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(result.out_params() == row_view()); +} + +BOOST_FIXTURE_TEST_CASE(output_params_bound, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_outparams(?, ?, ?)"); + + // Call the procedure + results result; + conn.execute_statement(stmt, std::make_tuple(10, nullptr, 30), result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 3u); + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(!result[0].is_out_params()); + check_meta(result[1].meta(), {column_type::int_, column_type::int_}); + BOOST_TEST(result[1].rows() == makerows(2, 10, 31)); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(result[1].is_out_params()); + BOOST_TEST(result[2].meta().size() == 0u); + BOOST_TEST(result[2].rows().size() == 0u); + BOOST_TEST(result[2].affected_rows() == 0u); + BOOST_TEST(result[2].warning_count() == 0u); + BOOST_TEST(result[2].last_insert_id() == 0u); + BOOST_TEST(result[2].info() == ""); + BOOST_TEST(!result[2].is_out_params()); + BOOST_TEST(result.out_params() == makerow(10, 31)); +} + +BOOST_FIXTURE_TEST_CASE(output_params_bound_multifn, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_outparams(?, ?, ?)"); + + // Call the procedure + execution_state st; + conn.start_statement_execution(stmt, std::make_tuple(10, nullptr, 30), st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + validate_2fields_meta(st.meta(), "one_row_table"); + + // 1st resultset, rows + auto rv = conn.read_some_rows(st); + BOOST_TEST(rv == makerows(2, 1, "f0")); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(!st.is_out_params()); + + // out params, head + conn.read_resultset_head(st); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::int_, column_type::int_}); + + // out params, rows and eof + rv = conn.read_some_rows(st); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(rv == makerows(2, 10, 31)); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); + BOOST_TEST(st.is_out_params()); + + // final eof + conn.read_resultset_head(st); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 0u); + BOOST_TEST(st.warning_count() == 0u); + BOOST_TEST(st.last_insert_id() == 0u); + BOOST_TEST(st.info() == ""); + BOOST_TEST(!st.is_out_params()); +} + +BOOST_FIXTURE_TEST_CASE(with_signal, tcp_network_fixture) +{ + connect(); + + // Statement + auto stmt = conn.prepare_statement("CALL sp_signal()"); + + // Call the procedure. It should fail, since we're invoking SIGNAL + results result; + error_code err; + diagnostics diag; + conn.execute_statement(stmt, std::make_tuple(), result, err, diag); + + // Verify results + BOOST_TEST(err == common_server_errc::er_no); + BOOST_TEST(diag.server_message() == "An error occurred"); +} + +BOOST_FIXTURE_TEST_CASE(with_query, tcp_network_fixture) +{ + connect(); + + // Call the procedure + results result; + conn.query("CALL sp_outparams(42, @var1, @var2)", result); + + // Verify results + BOOST_TEST_REQUIRE(result.size() == 2u); + validate_2fields_meta(result[0].meta(), "one_row_table"); + BOOST_TEST(result[0].rows() == makerows(2, 1, "f0")); + BOOST_TEST(result[1].meta().size() == 0u); + BOOST_TEST(result[1].rows().size() == 0u); + BOOST_TEST(result[1].affected_rows() == 0u); + BOOST_TEST(result[1].warning_count() == 0u); + BOOST_TEST(result[1].last_insert_id() == 0u); + BOOST_TEST(result[1].info() == ""); + BOOST_TEST(result.out_params() == row_view()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace diff --git a/test/integration/utils/include/er_connection.hpp b/test/integration/utils/include/er_connection.hpp index c92afb09..d23c5a17 100644 --- a/test/integration/utils/include/er_connection.hpp +++ b/test/integration/utils/include/er_connection.hpp @@ -65,6 +65,7 @@ public: execution_state& st ) = 0; virtual network_result close_statement(statement&) = 0; + virtual network_result read_resultset_head(execution_state& st) = 0; virtual network_result read_some_rows(execution_state& st) = 0; virtual network_result ping() = 0; virtual network_result quit() = 0; diff --git a/test/integration/utils/src/async_coroutinescpp20.cpp b/test/integration/utils/src/async_coroutinescpp20.cpp index 0ba3b10a..860ab176 100644 --- a/test/integration/utils/src/async_coroutinescpp20.cpp +++ b/test/integration/utils/src/async_coroutinescpp20.cpp @@ -124,6 +124,7 @@ function_table create_table() BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_start_statement_execution), BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_start_statement_execution), BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_close_statement), + BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_read_resultset_head), BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_read_some_rows), BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_ping), BOOST_MYSQL_ASYNC_COROCPP20_TABLE_ENTRY(async_quit), diff --git a/test/integration/utils/src/er_impl_common.hpp b/test/integration/utils/src/er_impl_common.hpp index ff8e6c03..be7b0cdd 100644 --- a/test/integration/utils/src/er_impl_common.hpp +++ b/test/integration/utils/src/er_impl_common.hpp @@ -26,6 +26,7 @@ #include "er_connection.hpp" #include "er_network_variant.hpp" #include "get_endpoint.hpp" +#include "network_result.hpp" #include "streams.hpp" namespace boost { @@ -58,6 +59,7 @@ struct function_table using start_stmt_execution_it_sig = network_result(conn_type&, const statement&, fv_list_it, fv_list_it, execution_state&); using close_stmt_sig = network_result(conn_type&, const statement&); + using read_resultset_head_sig = network_result(conn_type&, execution_state&); using read_some_rows_sig = network_result(conn_type&, execution_state&); using ping_sig = network_result(conn_type&); using quit_sig = network_result(conn_type&); @@ -72,6 +74,7 @@ struct function_table std::function start_stmt_execution_tuple; std::function start_stmt_execution_it; std::function close_stmt; + std::function read_resultset_head; std::function read_some_rows; std::function ping; std::function quit; @@ -100,6 +103,7 @@ function_table create_sync_table() Netmaker::template type::call(&conn_type::start_statement_execution), Netmaker::template type::call(&conn_type::start_statement_execution), Netmaker::template type::call(&conn_type::close_statement), + Netmaker::template type::call(&conn_type::read_resultset_head), Netmaker::template type::call(&conn_type::read_some_rows), Netmaker::template type::call(&conn_type::ping), Netmaker::template type::call(&conn_type::quit), @@ -126,6 +130,7 @@ function_table create_async_table() Netmaker::template type::call(&conn_type::async_start_statement_execution), Netmaker::template type::call(&conn_type::async_start_statement_execution), Netmaker::template type::call(&conn_type::async_close_statement), + Netmaker::template type::call(&conn_type::async_read_resultset_head), Netmaker::template type::call(&conn_type::async_read_some_rows), Netmaker::template type::call(&conn_type::async_ping), Netmaker::template type::call(&conn_type::async_quit), @@ -253,6 +258,10 @@ public: return table_.start_stmt_execution_it(conn_, stmt, params_first, params_last, st); } network_result close_statement(statement& stmt) override { return table_.close_stmt(conn_, stmt); } + network_result read_resultset_head(execution_state& st) override + { + return table_.read_resultset_head(conn_, st); + } network_result read_some_rows(execution_state& st) override { return table_.read_some_rows(conn_, st); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 6611675b..9b2df3a5 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -17,15 +17,7 @@ add_executable( detail/auxiliar/rows_iterator.cpp detail/auxiliar/field_type_traits.cpp detail/auxiliar/datetime.cpp - detail/network_algorithms/start_execution_generic.cpp - detail/network_algorithms/start_query.cpp - detail/network_algorithms/start_statement_execution.cpp - detail/network_algorithms/read_some_rows.cpp - detail/network_algorithms/read_all_rows.cpp - detail/network_algorithms/query.cpp - detail/network_algorithms/execute_statement.cpp - detail/network_algorithms/close_statement.cpp - detail/network_algorithms/ping.cpp + detail/auxiliar/row_impl.cpp detail/protocol/capabilities.cpp detail/protocol/null_bitmap_traits.cpp detail/protocol/deserialize_errc.cpp @@ -33,7 +25,16 @@ add_executable( detail/protocol/deserialize_text_field.cpp detail/protocol/deserialize_binary_field.cpp detail/protocol/deserialize_row.cpp + detail/protocol/deserialize_execute_response.cpp detail/protocol/process_error_packet.cpp + detail/protocol/execution_state_impl.cpp + detail/network_algorithms/read_resultset_head.cpp + detail/network_algorithms/start_execution.cpp + detail/network_algorithms/read_some_rows.cpp + detail/network_algorithms/execute.cpp + detail/network_algorithms/high_level_execution.cpp + detail/network_algorithms/close_statement.cpp + detail/network_algorithms/ping.cpp date.cpp datetime.cpp field_view.cpp @@ -51,9 +52,12 @@ add_executable( statement.cpp execution_state.cpp results.cpp + resultset_view.cpp + resultset.cpp connection.cpp - regressions.cpp throw_on_error.cpp + regressions.cpp + multifn.cpp entry_point.cpp ) target_link_libraries( diff --git a/test/unit/Jamfile b/test/unit/Jamfile index 25f58f4e..68ce8ff7 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -36,15 +36,7 @@ unit-test boost_mysql_unittests detail/auxiliar/rows_iterator.cpp detail/auxiliar/field_type_traits.cpp detail/auxiliar/datetime.cpp - detail/network_algorithms/start_execution_generic.cpp - detail/network_algorithms/start_query.cpp - detail/network_algorithms/start_statement_execution.cpp - detail/network_algorithms/read_some_rows.cpp - detail/network_algorithms/read_all_rows.cpp - detail/network_algorithms/query.cpp - detail/network_algorithms/execute_statement.cpp - detail/network_algorithms/close_statement.cpp - detail/network_algorithms/ping.cpp + detail/auxiliar/row_impl.cpp detail/protocol/capabilities.cpp detail/protocol/null_bitmap_traits.cpp detail/protocol/deserialize_errc.cpp @@ -52,7 +44,16 @@ unit-test boost_mysql_unittests detail/protocol/deserialize_text_field.cpp detail/protocol/deserialize_binary_field.cpp detail/protocol/deserialize_row.cpp + detail/protocol/deserialize_execute_response.cpp detail/protocol/process_error_packet.cpp + detail/protocol/execution_state_impl.cpp + detail/network_algorithms/read_resultset_head.cpp + detail/network_algorithms/start_execution.cpp + detail/network_algorithms/read_some_rows.cpp + detail/network_algorithms/execute.cpp + detail/network_algorithms/high_level_execution.cpp + detail/network_algorithms/close_statement.cpp + detail/network_algorithms/ping.cpp date.cpp datetime.cpp field_view.cpp @@ -70,9 +71,13 @@ unit-test boost_mysql_unittests statement.cpp execution_state.cpp results.cpp - regressions.cpp + resultset_view.cpp + resultset.cpp connection.cpp throw_on_error.cpp + regressions.cpp + multifn.cpp + entry_point.cpp : msvc:-FI"pch.hpp" # https://github.com/boostorg/boost/issues/711 ; diff --git a/test/unit/connection.cpp b/test/unit/connection.cpp index c30e68a5..764b67c7 100644 --- a/test/unit/connection.cpp +++ b/test/unit/connection.cpp @@ -19,7 +19,7 @@ #include #include -#include "create_message.hpp" +#include "creation/create_message.hpp" #include "printing.hpp" #include "test_connection.hpp" #include "unit_netfun_maker.hpp" @@ -87,7 +87,7 @@ BOOST_AUTO_TEST_CASE(use_move_constructed_connection) test_connection conn; // Use it - conn.stream().add_message(create_ok_packet_message(1)); + conn.stream().add_message(ok_msg_builder().seqnum(1).build_ok()); results result; fns.query(conn, "SELECT * FROM myt", result).validate_no_error(); @@ -95,7 +95,7 @@ BOOST_AUTO_TEST_CASE(use_move_constructed_connection) test_connection conn2(std::move(conn)); // Using it works (no dangling pointer) - conn2.stream().add_message(create_ok_packet_message(1, 42)); + conn2.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(42).build_ok()); fns.query(conn2, "DELETE FROM myt", result).validate_no_error(); BOOST_TEST(result.affected_rows() == 42u); } @@ -131,8 +131,8 @@ BOOST_AUTO_TEST_CASE(use_move_assigned_connection) test_connection conn2; // Use them - conn1.stream().add_message(create_ok_packet_message(1)); - conn2.stream().add_message(create_ok_packet_message(1)); + conn1.stream().add_message(ok_msg_builder().seqnum(1).build_ok()); + conn2.stream().add_message(ok_msg_builder().seqnum(1).build_ok()); results result; fns.query(conn1, "SELECT * FROM myt", result).validate_no_error(); fns.query(conn2, "SELECT * FROM myt", result).validate_no_error(); @@ -141,7 +141,7 @@ BOOST_AUTO_TEST_CASE(use_move_assigned_connection) conn2 = std::move(conn1); // Using it works (no dangling pointer) - conn2.stream().add_message(create_ok_packet_message(1, 42)); + conn2.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(42).build_ok()); fns.query(conn2, "DELETE FROM myt", result).validate_no_error(); BOOST_TEST(result.affected_rows() == 42u); } diff --git a/test/unit/detail/auxiliar/row_impl.cpp b/test/unit/detail/auxiliar/row_impl.cpp new file mode 100644 index 00000000..1f25cdda --- /dev/null +++ b/test/unit/detail/auxiliar/row_impl.cpp @@ -0,0 +1,653 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include "assert_buffer_equals.hpp" +#include "test_common.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; +using detail::row_impl; + +namespace { + +template +row_impl makerowimpl(T&&... args) +{ + auto fields = make_fv_arr(std::forward(args)...); + return row_impl(fields.data(), fields.size()); +} + +template +void add_fields(row_impl& r, T&&... args) +{ + auto fields = make_fv_arr(std::forward(args)...); + std::copy(fields.begin(), fields.end(), r.add_fields(fields.size())); +} + +// An array and vector with all scalar types +std::vector make_scalar_vector() +{ + return make_fv_vector( + 42, + 42u, + 4.2f, + 4.2, + date(2020, 1, 2), + datetime(2020, 10, 10), + maket(10, 1, 1), + nullptr + ); +} + +void hard_clear(std::vector& res) +{ + for (auto& f : res) + f = field_view(); +} + +// Check that references to certain row_impl's contents survive an operation +struct reference_checker +{ + const field_view* ptr; + + reference_checker(const row_impl& r) : ptr(r.fields().data()) {} + + void check(const row_impl& new_row) { BOOST_TEST(ptr == new_row.fields().data()); } +}; + +struct reference_checker_strs : reference_checker +{ + std::size_t string_index, blob_index; // indices in the row where a string/blob field resides + const char* string_ptr; + const unsigned char* blob_ptr; + + static void assert_ptrs_equal(const char* ptr1, const char* ptr2) + { + // Otherwise, UTF thinks it's a C string, tries to print it and causes Valgrind errors + BOOST_TEST(static_cast(ptr1) == static_cast(ptr2)); + } + + reference_checker_strs(const row_impl& r, std::size_t string_index, std::size_t blob_index) + : reference_checker(r), + string_index(string_index), + blob_index(blob_index), + string_ptr(r.fields().at(string_index).as_string().data()), + blob_ptr(r.fields().at(blob_index).as_blob().data()) + { + } + + void check(const row_impl& new_row) + { + reference_checker::check(new_row); + assert_ptrs_equal(string_ptr, new_row.fields().at(string_index).as_string().data()); + BOOST_TEST(blob_ptr == new_row.fields().at(blob_index).as_blob().data()); + } +}; + +BOOST_AUTO_TEST_SUITE(test_row_impl) + +BOOST_AUTO_TEST_CASE(default_ctor) +{ + row_impl r; + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_SUITE(ctor_from_span) +BOOST_AUTO_TEST_CASE(empty) +{ + std::vector fields; + row_impl r(fields.data(), fields.size()); + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + auto fields = make_scalar_vector(); + row_impl r(fields.data(), fields.size()); + + // Fields still valid even when the original source of the view changed + hard_clear(fields); + BOOST_TEST(r.fields() == make_scalar_vector()); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + std::string s1("test"), s2("othertest"); + blob b{0x00, 0xab, 0xf5}; + auto fields = make_fv_arr(s1, s2, 50, b); + row_impl r(fields.data(), fields.size()); + + // Fields still valid even when the original strings changed + s1 = "jkiop"; + s2 = "abcdef"; + b = {0xff, 0xa4, 0x02}; + BOOST_TEST(r.fields().size() == 4u); + BOOST_TEST(r.fields()[0] == field_view("test")); + BOOST_TEST(r.fields()[1] == field_view("othertest")); + BOOST_TEST(r.fields()[2] == field_view(50)); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(r.fields()[3].as_blob(), blob({0x00, 0xab, 0xf5})); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + std::string s; + blob b; + auto fields = make_fv_arr(s, 50, b); + row_impl r(fields.data(), fields.size()); + + // Fields still valid even when the original strings changed + s = "other"; + b = {0xff, 0xa4, 0x02}; + BOOST_TEST(r.fields().size() == 3u); + BOOST_TEST(r.fields()[0] == field_view("")); + BOOST_TEST(r.fields()[1] == field_view(50)); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(r.fields()[2].as_blob(), blob()); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(copy_ctor) +BOOST_AUTO_TEST_CASE(empty) +{ + row_impl r1; + row_impl r2(r1); + r1 = makerowimpl(42, "test"); // r2 should be independent of r1 + + BOOST_TEST(r2.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + auto fields = make_scalar_vector(); + row_impl r1(fields.data(), fields.size()); + row_impl r2(r1); + r1 = makerowimpl(42, "test"); // r2 should be independent of r1 + + BOOST_TEST(r2.fields() == make_scalar_vector()); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + row_impl r1 = makerowimpl("", 42, "test", makebv("\0\3\2")); + row_impl r2(r1); + r1 = makerowimpl("another_string", 4.2f, ""); // r2 should be independent of r1 + + BOOST_TEST(r2.fields().size() == 4u); + BOOST_TEST(r2.fields()[0] == field_view("")); + BOOST_TEST(r2.fields()[1] == field_view(42)); + BOOST_TEST(r2.fields()[2] == field_view("test")); + BOOST_TEST(r2.fields()[3] == field_view(makebv("\0\3\2"))); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + row_impl r1 = makerowimpl("", 42, blob_view()); + row_impl r2(r1); + r1 = makerowimpl("another_string", 4.2f, ""); // r2 should be independent of r1 + + BOOST_TEST(r2.fields().size() == 3u); + BOOST_TEST(r2.fields()[0] == field_view("")); + BOOST_TEST(r2.fields()[1] == field_view(42)); + BOOST_TEST(r2.fields()[2] == field_view(blob_view())); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(move_ctor) +BOOST_AUTO_TEST_CASE(empty) +{ + row_impl r1; + + // References should remain valid + reference_checker refcheck(r1); + + row_impl r2(std::move(r1)); + r1 = makerowimpl(42, "test"); // r2 should be independent of r1 + + BOOST_TEST(r2.fields().empty()); + refcheck.check(r2); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + auto fields = make_scalar_vector(); + row_impl r1(fields.data(), fields.size()); + + // References, pointers, etc. should remain valid + reference_checker refcheck(r1); + + row_impl r2(std::move(r1)); + r1 = makerowimpl(42, "test"); // r2 should be independent of r1 + + BOOST_TEST(r2.fields() == make_scalar_vector()); + refcheck.check(r2); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + row_impl r1 = makerowimpl("", 42, "test", makebv("\0\5\xff")); + + // References, pointers, etc should remain valid + reference_checker_strs refcheck(r1, 2, 3); + + // Move + row_impl r2(std::move(r1)); + r1 = makerowimpl("another_string", 4.2f, "", makebv("\1\5\xab")); // r2 should be independent of r1 + + BOOST_TEST(r2.fields().size() == 4u); + BOOST_TEST(r2.fields()[0] == field_view("")); + BOOST_TEST(r2.fields()[1] == field_view(42)); + BOOST_TEST(r2.fields()[2] == field_view("test")); + BOOST_TEST(r2.fields()[3] == field_view(makebv("\0\5\xff"))); + + // References, pointers, etc still valid + refcheck.check(r2); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + row_impl r1 = makerowimpl("", 42, blob_view()); + + // Move + row_impl r2(std::move(r1)); + r1 = makerowimpl("another_string", 4.2f, "", makebv("\1\5\xab")); // r2 should be independent of r1 + + BOOST_TEST(r2.fields() == make_fv_vector("", 42, blob_view())); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(copy_assignment) +BOOST_AUTO_TEST_CASE(empty) +{ + row_impl r1 = makerowimpl(42, "abcdef"); + row_impl r2; + r1 = r2; + r2 = makerowimpl(90, nullptr); // r1 is independent of r2 + BOOST_TEST(r1.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + auto fields = make_scalar_vector(); + row_impl r1 = makerowimpl(42, "abcdef"); + row_impl r2(fields.data(), fields.size()); + r1 = r2; + r2 = makerowimpl("abc", 80, nullptr); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_scalar_vector()); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + row_impl r1 = makerowimpl(42, "abcdef", makebv("\0\1\2")); + row_impl r2 = makerowimpl("a_very_long_string", nullptr, "", makebv("\3\4\5")); + r1 = r2; + r2 = makerowimpl("another_string", 90, "yet_another"); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_fv_vector("a_very_long_string", nullptr, "", makebv("\3\4\5"))); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + row_impl r1 = makerowimpl(42, "abcdef", makebv("\0\1\2")); + row_impl r2 = makerowimpl(nullptr, "", blob_view()); + r1 = r2; + r2 = makerowimpl("another_string", 90, "yet_another"); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_fv_vector(nullptr, "", blob_view())); +} + +BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) +{ + row_impl r1; + row_impl r2 = makerowimpl("abc", nullptr, "bcd", makebv("\1\2\3")); + r1 = r2; + + BOOST_TEST(r1.fields() == make_fv_vector("abc", nullptr, "bcd", makebv("\1\2\3"))); +} + +BOOST_AUTO_TEST_CASE(self_assignment_empty) +{ + row_impl r; + const row_impl& ref = r; + r = ref; + + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +{ + row_impl r = makerowimpl("abc", 50u, "fgh"); + const row_impl& ref = r; + r = ref; + + BOOST_TEST(r.fields() == make_fv_vector("abc", 50u, "fgh")); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(move_assignment) +BOOST_AUTO_TEST_CASE(empty) +{ + row_impl r1 = makerowimpl(42, "abcdef"); + row_impl r2; + + reference_checker refcheck(r2); + + r1 = std::move(r2); + r2 = makerowimpl(90, nullptr); // r1 is independent of r2 + BOOST_TEST(r1.fields().empty()); + refcheck.check(r1); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + auto fields = make_scalar_vector(); + row_impl r1 = makerowimpl(42, "abcdef"); + row_impl r2(fields.data(), fields.size()); + + // References should remain valid + reference_checker refcheck(r2); + + r1 = std::move(r2); + r2 = makerowimpl("abc", 80, nullptr); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_scalar_vector()); + refcheck.check(r1); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + row_impl r1 = makerowimpl(42, "abcdef", makebv("\0\4\1")); + row_impl r2 = makerowimpl("a_very_long_string", nullptr, "", makebv("\7\1\2")); + + // References, pointers, etc should remain valid + reference_checker_strs refcheck(r2, 0, 3); + + // Move + r1 = std::move(r2); + r2 = makerowimpl("another_string", 90, "yet_another", makebv("\0\0")); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_fv_vector("a_very_long_string", nullptr, "", makebv("\7\1\2"))); + refcheck.check(r1); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + row_impl r1 = makerowimpl(42, "abcdef", makebv("\0\4\1")); + row_impl r2 = makerowimpl("", blob_view()); + + // References, pointers, etc should remain valid + reference_checker_strs refcheck(r2, 0, 1); + + // Move + r1 = std::move(r2); + r2 = makerowimpl("another_string", 90); // r1 is independent of r2 + + BOOST_TEST(r1.fields() == make_fv_vector("", blob_view())); + refcheck.check(r1); +} + +BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) +{ + row_impl r1; + row_impl r2 = makerowimpl("abc", nullptr, "bcd", makebv("\0\2\5")); + + // References, pointers, etc should remain valid + reference_checker_strs refcheck(r2, 2, 3); + + r1 = std::move(r2); + + BOOST_TEST(r1.fields() == make_fv_vector("abc", nullptr, "bcd", makebv("\0\2\5"))); + refcheck.check(r1); +} + +BOOST_AUTO_TEST_CASE(self_assignment_empty) +{ + row_impl r; + row_impl&& ref = std::move(r); + r = std::move(ref); + + // r is in a valid but unspecified state; can be assigned to + r = makerowimpl("abcdef"); + BOOST_TEST(r.fields() == make_fv_vector("abcdef")); +} + +BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +{ + row_impl r = makerowimpl("abc", 50u, "fgh", makebv("\0\4")); + row_impl&& ref = std::move(r); + r = std::move(ref); // this should leave r in a valid but unspecified state + + // r is in a valid but unspecified state; can be assigned to + r = makerowimpl("abcdef"); + BOOST_TEST(r.fields() == make_fv_vector("abcdef")); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(assignment_from_span) +BOOST_AUTO_TEST_CASE(empty) +{ + row_impl r = makerowimpl(42, "abcdef", makebv("\0\xae")); + r.assign(nullptr, 0); + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(empty_non_null) +{ + field_view f; + row_impl r = makerowimpl(42, "abcdef", makebv("\0\xae")); + r.assign(&f, 0); + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(non_strings) +{ + row_impl r = makerowimpl(42, "abcdef"); + auto fields = make_scalar_vector(); + r.assign(fields.data(), fields.size()); + hard_clear(fields); // r should be independent of the original fields + + BOOST_TEST(r.fields() == make_scalar_vector()); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + std::string s1("a_very_long_string"), s2("abc"); + blob b{0x00, 0xfa}; + row_impl r = makerowimpl(42, "haksj", makebv("\0\1")); + auto fields = make_fv_arr(s1, nullptr, s2, b); + + r.assign(fields.data(), fields.size()); + fields = make_fv_arr("abc", 42u, 9, nullptr); // r should be independent of the original fields + s1 = "another_string"; // r should be independent of the original strings + s2 = "yet_another"; + b = {0xac, 0x32, 0x21, 0x50}; + + BOOST_TEST(r.fields() == make_fv_vector("a_very_long_string", nullptr, "abc", makebv("\0\xfa"))); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + std::string s(""); + blob b; + row_impl r = makerowimpl(42, "haksj", makebv("\0\1")); + auto fields = make_fv_arr(s, b); + + r.assign(fields.data(), fields.size()); + fields = make_fv_arr(0, 0); // r should be independent of the original fields + s = "another_string"; // r should be independent of the original strings + b = {0xac, 0x32, 0x21, 0x50}; + + BOOST_TEST(r.fields() == make_fv_vector("", blob_view())); +} + +BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) +{ + row_impl r; + auto fields = make_fv_arr("abc", nullptr, "bcd", makebv("\0\3")); + r.assign(fields.data(), fields.size()); + + BOOST_TEST(r.fields() == make_fv_vector("abc", nullptr, "bcd", makebv("\0\3"))); +} + +BOOST_AUTO_TEST_CASE(self_assignment) +{ + row_impl r = makerowimpl("abcdef", 42, "plk", makebv("\0\1")); + r.assign(r.fields().data(), r.fields().size()); + + BOOST_TEST(r.fields() == make_fv_vector("abcdef", 42, "plk", makebv("\0\1"))); +} + +BOOST_AUTO_TEST_CASE(self_assignment_empty) +{ + row_impl r; + r.assign(r.fields().data(), r.fields().size()); + BOOST_TEST(r.fields().empty()); +} + +BOOST_AUTO_TEST_CASE(self_assignment_cleared) +{ + row_impl r = makerowimpl(42, "abc"); + r.clear(); + r.assign(r.fields().data(), r.fields().size()); + BOOST_TEST(r.fields().empty()); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(add_fields_) +BOOST_AUTO_TEST_CASE(empty_collection) +{ + row_impl r; + field_view* storage = r.add_fields(2); + BOOST_TEST(r.fields().size() == 2u); + BOOST_TEST(storage == r.fields().data()); +} + +BOOST_AUTO_TEST_CASE(non_empty_collection) +{ + row_impl r = makerowimpl(nullptr, nullptr); + field_view* storage = r.add_fields(3); + BOOST_TEST(r.fields().size() == 5u); + BOOST_TEST(storage == r.fields().data() + 2); +} + +BOOST_AUTO_TEST_CASE(zero_fields) +{ + row_impl r = makerowimpl(nullptr, nullptr); + field_view* storage = r.add_fields(0); + BOOST_TEST(r.fields().size() == 2u); + BOOST_TEST(storage == r.fields().data() + 2); +} + +BOOST_AUTO_TEST_CASE(empty_collection_zero_fields) +{ + row_impl r; + field_view* storage = r.add_fields(0); + BOOST_TEST(r.fields().size() == 0u); + BOOST_TEST(storage == r.fields().data()); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(copy_strings_as_offsets) +BOOST_AUTO_TEST_CASE(scalars) +{ + row_impl r; + add_fields(r, nullptr, 42, 10.0f, date(2020, 10, 1)); + r.copy_strings_as_offsets(0, 4); + r.offsets_to_string_views(); + BOOST_TEST(r.fields() == make_fv_vector(nullptr, 42, 10.f, date(2020, 10, 1))); +} + +BOOST_AUTO_TEST_CASE(strings_blobs) +{ + row_impl r; + std::string s = "abc"; + blob b{0x01, 0x02, 0x03}; + add_fields(r, nullptr, s, 10.f, b); + r.copy_strings_as_offsets(1, 3); + s = "ghi"; + b = {0xff, 0xff, 0xff}; + r.offsets_to_string_views(); + BOOST_TEST(r.fields() == make_fv_vector(nullptr, "abc", 10.f, makebv("\1\2\3"))); +} + +BOOST_AUTO_TEST_CASE(empty_strings_blobs) +{ + row_impl r; + std::string s = ""; + blob b{}; + add_fields(r, nullptr, s, 10.f, b); + r.copy_strings_as_offsets(1, 3); + s = "ghi"; + b = {0xff, 0xff, 0xff}; + r.offsets_to_string_views(); + BOOST_TEST(r.fields() == make_fv_vector(nullptr, "", 10.f, makebv(""))); +} + +BOOST_AUTO_TEST_CASE(buffer_relocation) +{ + row_impl r; + std::string s = "abc"; + add_fields(r, nullptr, s); + r.copy_strings_as_offsets(0, 2); + s = "ghi"; + + blob b{0x01, 0x02, 0x03}; + add_fields(r, 10.f, b); + r.copy_strings_as_offsets(2, 2); + + s = ""; + b = {}; + add_fields(r, s, b); + r.copy_strings_as_offsets(4, 2); + b = {0x01, 0x02}; + + s = "this is a long string"; + add_fields(r, s); + r.copy_strings_as_offsets(6, 1); + s = "another long string"; + + r.offsets_to_string_views(); + BOOST_TEST( + r.fields() == + make_fv_vector(nullptr, "abc", 10.f, makebv("\1\2\3"), "", makebv(""), "this is a long string") + ); +} + +BOOST_AUTO_TEST_CASE(empty_range) +{ + std::string s = "abc"; + row_impl r = makerowimpl(nullptr, 42); + r.copy_strings_as_offsets(0, 0); + r.offsets_to_string_views(); + BOOST_TEST(r.fields() == make_fv_vector(nullptr, 42)); +} + +BOOST_AUTO_TEST_CASE(empty_collection) +{ + row_impl r; + r.copy_strings_as_offsets(0, 0); + r.offsets_to_string_views(); + BOOST_TEST(r.fields().empty()); +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace diff --git a/test/unit/detail/channel/message_parser.cpp b/test/unit/detail/channel/message_parser.cpp index 8be2e4e7..1a08e7ce 100644 --- a/test/unit/detail/channel/message_parser.cpp +++ b/test/unit/detail/channel/message_parser.cpp @@ -19,7 +19,7 @@ #include "assert_buffer_equals.hpp" #include "buffer_concat.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" using boost::asio::buffer; using boost::mysql::detail::message_parser; @@ -57,10 +57,7 @@ public: } boost::asio::const_buffer check_message(const std::vector& contents) { - auto msg = boost::asio::buffer( - buff_.current_message_first() - contents.size(), - contents.size() - ); + auto msg = boost::asio::buffer(buff_.current_message_first() - contents.size(), contents.size()); BOOST_MYSQL_ASSERT_BUFFER_EQUALS(msg, boost::asio::buffer(contents)); return msg; } @@ -259,8 +256,7 @@ BOOST_AUTO_TEST_CASE(three_messages_last_fragmented) std::vector first_msg_body{0x01, 0x02, 0x03}; std::vector second_msg_body{0x04, 0x05, 0x06, 0x07}; std::vector third_msg_body{0x08, 0x09}; - parser_fixture fixture(create_message(0, first_msg_body, 2, second_msg_body, 3, third_msg_body) - ); + parser_fixture fixture(create_message(0, first_msg_body, 2, second_msg_body, 3, third_msg_body)); // 1st message auto res = fixture.parse_bytes(20); // 1st and 2nd messages + 3rd message header and body part @@ -344,14 +340,7 @@ BOOST_AUTO_TEST_CASE(two_frame_message_with_reserved_area) // message to be parsed std::vector first_msg_body{0x01, 0x02, 0x03}; parser_fixture fixture( - create_message( - 0, - first_msg_body, - 4, - std::vector(64, 0x04), - 5, - {0x05, 0x06, 0x07} - ), + create_message(0, first_msg_body, 4, std::vector(64, 0x04), 5, {0x05, 0x06, 0x07}), 64 + 64 ); auto second_msg_body = concat_copy(std::vector(64, 0x04), {0x05, 0x06, 0x07}); @@ -576,14 +565,7 @@ BOOST_AUTO_TEST_CASE(two_frame_max_size) // message to be parsed. The two frames have size == max_frame_size, // so a third, empty header is received parser_fixture fixture( - create_message( - 1, - std::vector(64, 0x04), - 2, - std::vector(64, 0x05), - 3, - {} - ), + create_message(1, std::vector(64, 0x04), 2, std::vector(64, 0x05), 3, {}), 64 * 3 ); auto expected_message = concat_copy( diff --git a/test/unit/detail/channel/message_reader.cpp b/test/unit/detail/channel/message_reader.cpp index 01594ec8..f4b0d543 100644 --- a/test/unit/detail/channel/message_reader.cpp +++ b/test/unit/detail/channel/message_reader.cpp @@ -20,8 +20,9 @@ #include "assert_buffer_equals.hpp" #include "buffer_concat.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" #include "test_stream.hpp" +#include "unit_netfun_maker.hpp" using boost::asio::buffer; using boost::mysql::client_errc; @@ -39,18 +40,12 @@ class reader_fns { public: virtual ~reader_fns() {} - virtual void read_some( - message_reader&, - test_stream&, - error_code&, - bool keep_messages = false - ) = 0; + virtual void read_some(message_reader&, test_stream&, error_code&) = 0; virtual boost::asio::const_buffer read_one( message_reader&, test_stream&, std::uint8_t& seqnum, - error_code& ec, - bool keep_messages = false + error_code& ec ) = 0; virtual const char* name() const noexcept = 0; }; @@ -58,24 +53,18 @@ public: class sync_reader_fns : public reader_fns { public: - void read_some( - message_reader& reader, - test_stream& stream, - error_code& err, - bool keep_messages = false - ) final override + void read_some(message_reader& reader, test_stream& stream, error_code& err) final override { - reader.read_some(stream, err, keep_messages); + reader.read_some(stream, err); } boost::asio::const_buffer read_one( message_reader& reader, test_stream& stream, std::uint8_t& seqnum, - error_code& ec, - bool keep_messages = false + error_code& ec ) final override { - return reader.read_one(stream, seqnum, ec, keep_messages); + return reader.read_one(stream, seqnum, ec); } const char* name() const noexcept final override { return "sync"; }; }; @@ -83,27 +72,19 @@ public: class async_reader_fns : public reader_fns { public: - void read_some( - message_reader& reader, - test_stream& stream, - error_code& err, - bool keep_messages = false - ) final override + void read_some(message_reader& reader, test_stream& stream, error_code& err) final override { boost::asio::io_context ctx; - reader.async_read_some( - stream, - boost::asio::bind_executor(ctx.get_executor(), [&](error_code ec) { err = ec; }), - keep_messages - ); + reader.async_read_some(stream, boost::asio::bind_executor(ctx.get_executor(), [&](error_code ec) { + err = ec; + })); ctx.run(); } boost::asio::const_buffer read_one( message_reader& reader, test_stream& stream, std::uint8_t& seqnum, - error_code& err, - bool keep_messages = false + error_code& err ) final override { boost::asio::io_context ctx; @@ -117,8 +98,7 @@ public: err = ec; res = b; } - ), - keep_messages + ) ); ctx.run(); return res; @@ -282,7 +262,7 @@ BOOST_AUTO_TEST_CASE(two_messages) } } -BOOST_AUTO_TEST_CASE(previous_message_keep_messages_false) +BOOST_AUTO_TEST_CASE(previous_message) { for (auto* fns : all_reader_fns) { @@ -299,14 +279,14 @@ BOOST_AUTO_TEST_CASE(previous_message_keep_messages_false) error_code err(client_errc::server_unsupported); // Read and get 1st message - fns->read_some(reader, stream, err, false); + fns->read_some(reader, stream, err); BOOST_TEST(err == error_code()); BOOST_REQUIRE(reader.has_message()); auto msg1 = reader.get_next_message(seqnum1, err); BOOST_REQUIRE(err == error_code()); // Read and get 2nd message - fns->read_some(reader, stream, err, false); + fns->read_some(reader, stream, err); BOOST_TEST(err == error_code()); BOOST_REQUIRE(reader.has_message()); auto msg2 = reader.get_next_message(seqnum2, err); @@ -322,46 +302,6 @@ BOOST_AUTO_TEST_CASE(previous_message_keep_messages_false) } } -BOOST_AUTO_TEST_CASE(previous_message_keep_messages_true) -{ - for (auto* fns : all_reader_fns) - { - BOOST_TEST_CONTEXT(fns->name()) - { - message_reader reader(512); - std::uint8_t seqnum1 = 2; - std::uint8_t seqnum2 = 5; - std::vector msg1_body{0x01, 0x02, 0x03}; - std::vector msg2_body{0x05, 0x06, 0x07}; - test_stream stream; - stream.add_message(create_message(seqnum1, msg1_body)); - stream.add_message(create_message(seqnum2, msg2_body)); - error_code err(client_errc::server_unsupported); - - // Read and get 1st message - fns->read_some(reader, stream, err, true); - BOOST_TEST(err == error_code()); - BOOST_REQUIRE(reader.has_message()); - auto msg1 = reader.get_next_message(seqnum1, err); - BOOST_REQUIRE(err == error_code()); - - // Read and get 2nd message - fns->read_some(reader, stream, err, true); - BOOST_TEST(err == error_code()); - BOOST_REQUIRE(reader.has_message()); - auto msg2 = reader.get_next_message(seqnum2, err); - BOOST_REQUIRE(err == error_code()); - BOOST_TEST(stream.num_unread_bytes() == 0u); - BOOST_TEST(seqnum2 == 6u); - BOOST_TEST(!reader.has_message()); - - // Both messages are valid - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(msg1, buffer(msg1_body)); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(msg2, buffer(msg2_body)); - } - } -} - BOOST_AUTO_TEST_CASE(error) { for (auto* fns : all_reader_fns) @@ -392,8 +332,7 @@ BOOST_AUTO_TEST_CASE(multiframe_message) test_stream stream( create_message(seqnum, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 3, {0x09, 0x0a}) ); - std::vector - expected_msg{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}; + std::vector expected_msg{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}; error_code err(client_errc::server_unsupported); // Read succesfully diff --git a/test/unit/detail/channel/message_writer.cpp b/test/unit/detail/channel/message_writer.cpp index 9c7bd6d6..b8bcb71c 100644 --- a/test/unit/detail/channel/message_writer.cpp +++ b/test/unit/detail/channel/message_writer.cpp @@ -18,7 +18,7 @@ #include "assert_buffer_equals.hpp" #include "buffer_concat.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" #include "test_stream.hpp" using boost::asio::buffer; diff --git a/test/unit/detail/channel/message_writer_processor.cpp b/test/unit/detail/channel/message_writer_processor.cpp index 875bf8c5..754e97dd 100644 --- a/test/unit/detail/channel/message_writer_processor.cpp +++ b/test/unit/detail/channel/message_writer_processor.cpp @@ -22,7 +22,7 @@ #include "assert_buffer_equals.hpp" #include "buffer_concat.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" using boost::asio::buffer; using boost::mysql::error_code; @@ -31,11 +31,7 @@ using boost::mysql::test::concat_copy; namespace { -void check_header( - boost::asio::const_buffer buff, - std::uint8_t expected_seqnum, - std::size_t expected_size -) +void check_header(boost::asio::const_buffer buff, std::uint8_t expected_seqnum, std::size_t expected_size) { BOOST_TEST_REQUIRE(buff.size() == 4u); boost::mysql::detail::deserialization_context ctx(buff, boost::mysql::detail::capabilities()); diff --git a/test/unit/detail/network_algorithms/close_statement.cpp b/test/unit/detail/network_algorithms/close_statement.cpp index 41b77cb5..21e24238 100644 --- a/test/unit/detail/network_algorithms/close_statement.cpp +++ b/test/unit/detail/network_algorithms/close_statement.cpp @@ -15,7 +15,7 @@ #include #include "assert_buffer_equals.hpp" -#include "create_statement.hpp" +#include "creation/create_statement.hpp" #include "run_coroutine.hpp" #include "test_connection.hpp" #include "unit_netfun_maker.hpp" @@ -47,7 +47,7 @@ constexpr std::uint8_t expected_message[]{0x05, 0x00, 0x00, 0x00, 0x19, 0x03, 0x struct fixture { test_connection conn; - statement stmt{create_statement(2, 3)}; + statement stmt{statement_builder().id(3).num_params(2).build()}; }; BOOST_AUTO_TEST_CASE(success) diff --git a/test/unit/detail/network_algorithms/execute.cpp b/test/unit/detail/network_algorithms/execute.cpp new file mode 100644 index 00000000..8f4c59d5 --- /dev/null +++ b/test/unit/detail/network_algorithms/execute.cpp @@ -0,0 +1,266 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include +#include +#include + +#include + +#include "assert_buffer_equals.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "creation/create_message_struct.hpp" +#include "creation/create_row_message.hpp" +#include "test_channel.hpp" +#include "test_common.hpp" +#include "test_stream.hpp" +#include "unit_netfun_maker.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; +using detail::protocol_field_type; +using detail::resultset_encoding; + +namespace { + +using netfun_maker = netfun_maker_fn; + +struct +{ + typename netfun_maker::signature execute; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&detail::execute), "sync" }, + {netfun_maker::async_errinfo(&detail::async_execute), "async"} +}; + +// Verify that we clear any previous result +results create_initial_results() +{ + return create_results({ + {{protocol_field_type::bit, protocol_field_type::var_string}, + makerows(2, 30, "abc", 40, "bhj"), + ok_builder().affected_rows(89).info("abc").build()} + }); +} + +BOOST_AUTO_TEST_SUITE(test_execute) + +BOOST_AUTO_TEST_CASE(empty_resultset) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto ok_packet = ok_msg_builder().affected_rows(60u).info("abc").seqnum(1).build_ok(); + auto chan = create_channel(ok_packet); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + + // Call the function + fns.execute(chan, resultset_encoding::binary, result).validate_no_error(); + + // We've written the exeuction request + auto expected_msg = create_message(0, {0x02, 0x05, 0x09}); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(chan.lowest_layer().bytes_written(), expected_msg); + + // We've populated the results + BOOST_TEST_REQUIRE(result.size() == 1u); + BOOST_TEST(result[0].affected_rows() == 60u); + BOOST_TEST(result[0].info() == "abc"); + BOOST_TEST(result[0].rows() == rows()); + BOOST_TEST(chan.shared_sequence_number() == 0u); // not used + } + } +} + +BOOST_AUTO_TEST_CASE(single_batch) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto chan = create_channel(); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + std::vector messages; + concat(messages, create_message(1, {0x01})); // OK, 1 metadata + concat(messages, create_coldef_message(2, protocol_field_type::longlong)); // meta + concat(messages, create_text_row_message(3, 42)); // row 1 + concat(messages, create_text_row_message(4, 43)); // row 2 + concat( + messages, + ok_msg_builder().seqnum(5).affected_rows(10u).info("1st").more_results(true).build_eof() + ); + concat( + messages, + ok_msg_builder().seqnum(6).affected_rows(20u).info("2nd").more_results(true).build_ok() + ); + concat(messages, create_message(7, {0x01})); // OK, 1 metadata + concat(messages, create_coldef_message(8, protocol_field_type::var_string)); // meta + concat(messages, create_text_row_message(9, "abc")); // row 1 + concat(messages, ok_msg_builder().seqnum(10).affected_rows(30u).info("3rd").build_eof()); + chan.lowest_layer().add_message(std::move(messages)); + + // Call the function + fns.execute(chan, resultset_encoding::text, result).validate_no_error(); + + // We've written the exeuction request + auto expected_msg = create_message(0, {0x02, 0x05, 0x09}); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(chan.lowest_layer().bytes_written(), expected_msg); + + // We've populated the results + BOOST_TEST_REQUIRE(result.size() == 3u); + BOOST_TEST(result[0].affected_rows() == 10u); + BOOST_TEST(result[0].info() == "1st"); + BOOST_TEST(result[0].rows() == makerows(1, 42, 43)); + BOOST_TEST(result[1].affected_rows() == 20u); + BOOST_TEST(result[1].info() == "2nd"); + BOOST_TEST(result[1].rows() == rows()); + BOOST_TEST(result[2].affected_rows() == 30u); + BOOST_TEST(result[2].info() == "3rd"); + BOOST_TEST(result[2].rows() == makerows(1, "abc")); + BOOST_TEST(chan.shared_sequence_number() == 0u); // not used + } + } +} + +BOOST_AUTO_TEST_CASE(multiple_batches) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto chan = create_channel(); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + chan.lowest_layer() + .add_message(create_message(1, {0x01})) // OK, 1 metadata + .add_message(create_coldef_message(2, protocol_field_type::tiny)) // meta + .add_message(create_text_row_message(3, 42)) // row 1 + .add_message(create_text_row_message(4, 43)) // row 2 + .add_message( + ok_msg_builder().seqnum(5).affected_rows(10u).info("1st").more_results(true).build_eof() + ) + .add_message( + ok_msg_builder().seqnum(6).affected_rows(20u).info("2nd").more_results(true).build_ok() + ) + .add_message(create_message(7, {0x01})) // OK, 1 metadata + .add_message(create_coldef_message(8, protocol_field_type::var_string)) // meta + .add_message(create_text_row_message(9, "ab")) // row 1 + .add_message(ok_msg_builder().seqnum(10).affected_rows(30u).info("3rd").build_eof()); + + // Call the function + fns.execute(chan, resultset_encoding::text, result).validate_no_error(); + + // We've written the exeuction request + auto expected_msg = create_message(0, {0x02, 0x05, 0x09}); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(chan.lowest_layer().bytes_written(), expected_msg); + + // We've populated the results + BOOST_TEST_REQUIRE(result.size() == 3u); + BOOST_TEST(result[0].affected_rows() == 10u); + BOOST_TEST(result[0].info() == "1st"); + BOOST_TEST(result[0].rows() == makerows(1, 42, 43)); + BOOST_TEST(result[1].affected_rows() == 20u); + BOOST_TEST(result[1].info() == "2nd"); + BOOST_TEST(result[1].rows() == rows()); + BOOST_TEST(result[2].affected_rows() == 30u); + BOOST_TEST(result[2].info() == "3rd"); + BOOST_TEST(result[2].rows() == makerows(1, "ab")); + BOOST_TEST(chan.shared_sequence_number() == 0u); // not used + } + } +} + +BOOST_AUTO_TEST_CASE(error_network_error) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + // Tests error on write, while reading head and while reading rows + for (std::size_t i = 0; i <= 2; ++i) + { + BOOST_TEST_CONTEXT("i=" << i) + { + auto result = create_initial_results(); + auto chan = create_channel(); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + chan.lowest_layer() + .add_message(concat_copy( + create_message(1, {0x01}), + create_coldef_message(2, protocol_field_type::tiny) + )) + .add_message(concat_copy( + create_text_row_message(3, 42), + ok_msg_builder().seqnum(4).info("1st").build_eof() + )) + .set_fail_count(fail_count(i, client_errc::wrong_num_params)); + + // Call the function + fns.execute(chan, resultset_encoding::text, result) + .validate_error_exact(client_errc::wrong_num_params); + } + } + } + } +} + +// Seqnum mismatch on row messages +BOOST_AUTO_TEST_CASE(error_seqnum_mismatch) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto chan = create_channel(); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + chan.lowest_layer().add_message(concat_copy( + create_message(1, {0x01}), + create_coldef_message(2, protocol_field_type::tiny), + create_text_row_message(3, 42), + ok_msg_builder().seqnum(0).info("1st").build_eof() + )); + + // Call the function + fns.execute(chan, resultset_encoding::text, result) + .validate_error_exact(client_errc::sequence_number_mismatch); + } + } +} + +BOOST_AUTO_TEST_CASE(error_deserializing_rows) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto chan = create_channel(); + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); // some execution request + chan.lowest_layer().add_message(concat_copy( + create_message(1, {0x01}), + create_coldef_message(2, protocol_field_type::tiny), + create_message(3, {0x02, 0xff}) // bad row + )); + + // Call the function + fns.execute(chan, resultset_encoding::text, result) + .validate_error_exact(client_errc::incomplete_message); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/execute_statement.cpp b/test/unit/detail/network_algorithms/execute_statement.cpp deleted file mode 100644 index 96679b22..00000000 --- a/test/unit/detail/network_algorithms/execute_statement.cpp +++ /dev/null @@ -1,245 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include - -#include -#include - -#include -#include - -#include - -#include "assert_buffer_equals.hpp" -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "create_statement.hpp" -#include "printing.hpp" -#include "run_coroutine.hpp" -#include "test_common.hpp" -#include "test_connection.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::blob; -using boost::mysql::client_errc; -using boost::mysql::column_type; -using boost::mysql::error_code; -using boost::mysql::results; -using boost::mysql::statement; -using boost::mysql::detail::connection_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::results_access; -using boost::mysql::detail::resultset_encoding; -using namespace boost::mysql::test; - -namespace { - -// Machinery to treat iterator and tuple overloads the same -using netfun_maker = netfun_maker_mem< - void, - test_connection, - const statement&, - const std::tuple&, - results&>; - -struct -{ - netfun_maker::signature execute_statement; - const char* name; -} all_fns[] = { - {netfun_maker::sync_errc(&test_connection::execute_statement), "sync_errc" }, - {netfun_maker::sync_exc(&test_connection::execute_statement), "sync_exc" }, - {netfun_maker::async_errinfo(&test_connection::async_execute_statement), "async_errinfo" }, - {netfun_maker::async_noerrinfo(&test_connection::async_execute_statement), "async_noerrinfo"}, -}; - -// Verify that we reset the results object -results create_initial_results() -{ - results res; - results_access::get_rows(res) = makerows(1, 42, "abc"); - results_access::get_state(res - ) = create_execution_state(resultset_encoding::text, {protocol_field_type::geometry}, 4); - return res; -} - -BOOST_AUTO_TEST_SUITE(test_execute_statement) - -BOOST_AUTO_TEST_CASE(success) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - auto stmt = create_statement(2); - conn.stream().add_message(create_ok_packet_message(1, 2, 3, 4, 5, "info")); - - // Call the function - fns.execute_statement(conn, stmt, std::make_tuple("test", nullptr), result).validate_no_error(); - - // Verify the message we sent - std::uint8_t expected_message[] = { - 0x015, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, - }; - BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), expected_message); - - // Verify the results - BOOST_TEST(result.meta().size() == 0u); - BOOST_TEST(result.affected_rows() == 2u); - BOOST_TEST(result.last_insert_id() == 3u); - BOOST_TEST(result.warning_count() == 5u); - BOOST_TEST(result.info() == "info"); - } - } -} - -BOOST_AUTO_TEST_CASE(error_start_statement_execution) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - auto stmt = create_statement(2); - conn.stream().set_fail_count(fail_count(0, client_errc::server_unsupported)); - - // Call the function - fns.execute_statement(conn, stmt, std::make_tuple("abc", nullptr), result) - .validate_error_exact(client_errc::server_unsupported); - } - } -} - -BOOST_AUTO_TEST_CASE(error_read_all_rows) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - auto stmt = create_statement(2); - conn.stream().add_message(create_message(1, {0x01})); // Response OK, 1 metadata packet - conn.stream().add_message(create_coldef_message(2, protocol_field_type::geometry)); - conn.stream().set_fail_count(fail_count(4, client_errc::server_unsupported)); - - // Call the function - fns.execute_statement(conn, stmt, std::make_tuple("abc", nullptr), result) - .validate_error_exact(client_errc::server_unsupported); - - // Ensure we successfully ran the start_query - BOOST_TEST_REQUIRE(results_access::get_state(result).meta().size() == 1u); - BOOST_TEST(results_access::get_state(result).meta()[0].type() == column_type::geometry); - } - } -} - -// Verify that we correctly perform a decay-copy of the parameters and the -// statement handle, relevant for deferred tokens -#ifdef BOOST_ASIO_HAS_CO_AWAIT -BOOST_AUTO_TEST_SUITE(deferred_lifetimes) -struct fixture -{ - results result{create_initial_results()}; - test_connection conn; - statement stmt{create_statement(2)}; - - static constexpr std::uint8_t expected_msg[]{ - 0x1d, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, - 0x00, 0x08, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }; - - fixture() { conn.stream().add_message(create_ok_packet_message(1)); } -}; - -BOOST_AUTO_TEST_CASE(params_rvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - auto aw = fix.conn.async_execute_statement( - fix.stmt, - std::make_tuple(std::string("test"), 42), - fix.result, - boost::asio::use_awaitable - ); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.result.rows().size() == 0u); - }); -} - -BOOST_AUTO_TEST_CASE(params_lvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - auto tup = std::make_tuple(std::string("test"), 42); - auto aw = fix.conn.async_execute_statement(fix.stmt, tup, fix.result, boost::asio::use_awaitable); - tup = std::make_tuple(std::string("other"), 90); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.result.rows().size() == 0u); - }); -} - -BOOST_AUTO_TEST_CASE(params_const_lvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - const auto tup = std::make_tuple(std::string("test"), 42); - auto aw = fix.conn.async_execute_statement(fix.stmt, tup, fix.result, boost::asio::use_awaitable); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.result.rows().size() == 0u); - }); -} - -// Verify that we don't require the passed-in statement to be alive -BOOST_AUTO_TEST_CASE(statement_handle) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - auto aw = fix.conn.async_execute_statement( - statement(fix.stmt), - std::make_tuple(std::string("test"), 42), - fix.result, - boost::asio::use_awaitable - ); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.result.rows().size() == 0u); - }); -} - -BOOST_AUTO_TEST_SUITE_END() -#endif - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/high_level_execution.cpp b/test/unit/detail/network_algorithms/high_level_execution.cpp new file mode 100644 index 00000000..ee8187d1 --- /dev/null +++ b/test/unit/detail/network_algorithms/high_level_execution.cpp @@ -0,0 +1,546 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include + +#include "assert_buffer_equals.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "creation/create_message_struct.hpp" +#include "creation/create_statement.hpp" +#include "printing.hpp" +#include "run_coroutine.hpp" +#include "test_common.hpp" +#include "test_connection.hpp" +#include "unit_netfun_maker.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using detail::protocol_field_type; +using detail::resultset_encoding; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_high_level_execution) + +// The serialized form of the "SELECT 1" query +constexpr std::uint8_t select_1_msg[] = + {0x09, 0x00, 0x00, 0x00, 0x03, 0x53, 0x45, 0x4c, 0x45, 0x43, 0x54, 0x20, 0x31}; + +// The serialized form of executing a statement with ID=1, params=("test", nullptr) +constexpr std::uint8_t execute_stmt_msg[] = { + 0x15, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, +}; + +// The statement to be executed +statement create_the_statement() { return statement_builder().id(1).num_params(2).build(); } + +// Verify that we clear any previous result +results create_initial_results() +{ + return create_results({ + { + {protocol_field_type::var_string}, + makerows(1, "abc", "def"), + ok_builder().affected_rows(42).info("prev").build(), + } + }); +} + +execution_state create_initial_state() +{ + std::vector fields; // won't be further used - can be left dangling + return exec_builder(false) + .reset(resultset_encoding::binary, &fields) + .meta({protocol_field_type::time}) + .seqnum(42) + .build_state(); +} + +// +// ------- query --------- +// +BOOST_AUTO_TEST_SUITE(query_) + +using netfun_maker = netfun_maker_mem; + +struct +{ + netfun_maker::signature query; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::query), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::query), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_query), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_query), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(10).info("1st").build_ok()); + + // Call the function + fns.query(conn, "SELECT 1", result).validate_no_error(); + + // Verify the message we sent + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), select_1_msg); + + // Verify the results + BOOST_TEST_REQUIRE(result.size() == 1u); + BOOST_TEST(result.meta().size() == 0u); + BOOST_TEST(result.affected_rows() == 10u); + BOOST_TEST(result.info() == "1st"); + } + } +} + +BOOST_AUTO_TEST_CASE(error) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + results result; + test_connection conn; + conn.stream().set_fail_count(fail_count(0, common_server_errc::er_aborting_connection)); + + // Call the function + fns.query(conn, "SELECT 1", result) + .validate_error_exact(common_server_errc::er_aborting_connection); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +// +// ------- start_query --------- +// +BOOST_AUTO_TEST_SUITE(start_query_) + +using netfun_maker = netfun_maker_mem; + +struct +{ + netfun_maker::signature start_query; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::start_query), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::start_query), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_start_query), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_start_query), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + std::vector fields; + auto st = create_initial_state(); + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(50).info("1st").build_ok()); + + // Call the function + fns.start_query(conn, "SELECT 1", st).validate_no_error(); + + // Verify the message we sent + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), select_1_msg); + + // Verify the results + BOOST_TEST(get_impl(st).encoding() == resultset_encoding::text); + BOOST_TEST(st.complete()); + BOOST_TEST(get_impl(st).sequence_number() == 2u); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 50u); + BOOST_TEST(st.info() == "1st"); + } + } +} + +BOOST_AUTO_TEST_CASE(error) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + test_connection conn; + conn.stream().set_fail_count(fail_count(0, common_server_errc::er_aborting_connection)); + + // Call the function + fns.start_query(conn, "SELECT 1", st) + .validate_error_exact(common_server_errc::er_aborting_connection); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +// +// ------- execute_statement --------- +// +BOOST_AUTO_TEST_SUITE(execute_statement_) + +using netfun_maker = netfun_maker_mem< + void, + test_connection, + const statement&, + const std::tuple&, + results&>; + +struct +{ + netfun_maker::signature execute_statement; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::execute_statement), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::execute_statement), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_execute_statement), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_execute_statement), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto stmt = create_the_statement(); + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(50).info("1st").build_ok()); + + // Call the function + fns.execute_statement(conn, stmt, std::make_tuple("test", nullptr), result).validate_no_error(); + + // Verify the message we sent + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + + // Verify the results + BOOST_TEST_REQUIRE(result.size() == 1u); + BOOST_TEST(result.meta().size() == 0u); + BOOST_TEST(result.affected_rows() == 50u); + BOOST_TEST(result.info() == "1st"); + } + } +} + +BOOST_AUTO_TEST_CASE(error_wrong_num_params) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto result = create_initial_results(); + auto stmt = statement_builder().id(1).num_params(3).build(); + test_connection conn; + + // Call the function + fns.execute_statement(conn, stmt, std::make_tuple("test", nullptr), result) + .validate_error_exact(client_errc::wrong_num_params); + } + } +} + +// Verify that we correctly perform a decay-copy of the parameters and the +// statement handle, relevant for deferred tokens +#ifdef BOOST_ASIO_HAS_CO_AWAIT +BOOST_AUTO_TEST_CASE(deferred_lifetimes_rvalues) +{ + run_coroutine([]() -> boost::asio::awaitable { + results result; + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).info("1st").build_ok()); + + // Deferred op + auto aw = conn.async_execute_statement( + create_the_statement(), // statement is a temporary + std::make_tuple(std::string("test"), nullptr), // tuple is a temporary + result, + boost::asio::use_awaitable + ); + co_await std::move(aw); + + // verify that the op had the intended effects + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + BOOST_TEST(result.info() == "1st"); + }); +} + +BOOST_AUTO_TEST_CASE(deferred_lifetimes_lvalues) +{ + run_coroutine([]() -> boost::asio::awaitable { + results result; + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).info("1st").build_ok()); + boost::asio::awaitable aw; + + // Deferred op + { + auto stmt = create_the_statement(); + auto params = std::make_tuple(std::string("test"), nullptr); + aw = conn.async_execute_statement(stmt, params, result, boost::asio::use_awaitable); + } + + co_await std::move(aw); + + // verify that the op had the intended effects + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + BOOST_TEST(result.info() == "1st"); + }); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() + +// +// ------- start_statement_execution (tuple) --------- +// +BOOST_AUTO_TEST_SUITE(start_statement_execution_tuple) + +using netfun_maker = netfun_maker_mem< + void, + test_connection, + const statement&, + const std::tuple&, + execution_state&>; + +struct +{ + netfun_maker::signature start_statement_execution; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::start_statement_execution), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::start_statement_execution), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_start_statement_execution), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_start_statement_execution), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto st = create_initial_state(); + auto stmt = create_the_statement(); + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(50).info("1st").build_ok()); + + // Call the function + fns.start_statement_execution(conn, stmt, std::make_tuple("test", nullptr), st) + .validate_no_error(); + + // Verify the message we sent + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + + // Verify the results + BOOST_TEST(get_impl(st).encoding() == resultset_encoding::binary); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(get_impl(st).sequence_number() == 2u); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 50u); + BOOST_TEST(st.info() == "1st"); + } + } +} + +BOOST_AUTO_TEST_CASE(error_wrong_num_params) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + auto stmt = statement_builder().id(1).num_params(3).build(); + test_connection conn; + + // Call the function + fns.start_statement_execution(conn, stmt, std::make_tuple("test", nullptr), st) + .validate_error_exact(client_errc::wrong_num_params); + } + } +} + +// Verify that we correctly perform a decay-copy of the parameters and the +// statement handle, relevant for deferred tokens +#ifdef BOOST_ASIO_HAS_CO_AWAIT +BOOST_AUTO_TEST_CASE(deferred_lifetimes_rvalues) +{ + run_coroutine([]() -> boost::asio::awaitable { + execution_state st; + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).info("1st").build_ok()); + + // Deferred op + auto aw = conn.async_start_statement_execution( + create_the_statement(), // statement is a temporary + std::make_tuple(std::string("test"), nullptr), // tuple is a temporary + st, + boost::asio::use_awaitable + ); + co_await std::move(aw); + + // verify that the op had the intended effects + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + BOOST_TEST(st.info() == "1st"); + }); +} + +BOOST_AUTO_TEST_CASE(deferred_lifetimes_lvalues) +{ + run_coroutine([]() -> boost::asio::awaitable { + execution_state st; + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).info("1st").build_ok()); + boost::asio::awaitable aw; + + // Deferred op + { + auto stmt = create_the_statement(); + auto params = std::make_tuple(std::string("test"), nullptr); + aw = conn.async_start_statement_execution(stmt, params, st, boost::asio::use_awaitable); + } + + co_await std::move(aw); + + // verify that the op had the intended effects + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + BOOST_TEST(st.info() == "1st"); + }); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() + +// +// ------- start_statement_execution (iterator) --------- +// +BOOST_AUTO_TEST_SUITE(start_statement_execution_it) + +using netfun_maker = netfun_maker_mem< + void, + test_connection, + const statement&, + const field_view*, + const field_view*, + execution_state&>; + +struct +{ + netfun_maker::signature start_statement_execution; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::start_statement_execution), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::start_statement_execution), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_start_statement_execution), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_start_statement_execution), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + auto st = create_initial_state(); + auto stmt = create_the_statement(); + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).affected_rows(50).info("1st").build_ok()); + + // Call the function + auto fields = make_fv_arr("test", nullptr); + fns.start_statement_execution(conn, stmt, fields.data(), fields.data() + fields.size(), st) + .validate_no_error(); + + // Verify the message we sent + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + + // Verify the results + BOOST_TEST(get_impl(st).encoding() == resultset_encoding::binary); + BOOST_TEST(st.complete()); + BOOST_TEST(get_impl(st).sequence_number() == 2u); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 50u); + BOOST_TEST(st.info() == "1st"); + } + } +} + +BOOST_AUTO_TEST_CASE(error_wrong_num_params) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + auto stmt = statement_builder().id(1).num_params(3).build(); + test_connection conn; + + // Call the function + auto fields = make_fv_arr("test", nullptr); + fns.start_statement_execution(conn, stmt, fields.data(), fields.data() + fields.size(), st) + .validate_error_exact(client_errc::wrong_num_params); + } + } +} + +// Verify that we correctly perform a decay-copy of the stmt handle +#ifdef BOOST_ASIO_HAS_CO_AWAIT +BOOST_AUTO_TEST_CASE(deferred_lifetimes) +{ + run_coroutine([]() -> boost::asio::awaitable { + execution_state st; + test_connection conn; + conn.stream().add_message(ok_msg_builder().seqnum(1).info("1st").build_ok()); + auto fields = make_fv_arr("test", nullptr); + + // Deferred op + auto aw = conn.async_start_statement_execution( + create_the_statement(), + fields.data(), + fields.data() + fields.size(), + st, + boost::asio::use_awaitable + ); + co_await std::move(aw); + + // verify that the op had the intended effects + BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), execute_stmt_msg); + BOOST_TEST(st.info() == "1st"); + }); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/ping.cpp b/test/unit/detail/network_algorithms/ping.cpp index 7a4b3e3e..9923e1c9 100644 --- a/test/unit/detail/network_algorithms/ping.cpp +++ b/test/unit/detail/network_algorithms/ping.cpp @@ -17,7 +17,8 @@ #include #include "assert_buffer_equals.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" +#include "creation/create_message_struct.hpp" #include "test_connection.hpp" #include "unit_netfun_maker.hpp" @@ -53,7 +54,7 @@ BOOST_AUTO_TEST_CASE(process_ping_response_) error_code expected_err; const char* expected_msg; } test_cases[] = { - {"success", create_ok_packet_body(), error_code(), ""}, + {"success", ok_msg_builder().build_body(), error_code(), ""}, {"empty_message", {}, client_errc::incomplete_message, ""}, {"invalid_message_type", {0xab}, client_errc::protocol_value_error, ""}, {"bad_ok_packet", {0x00, 0x01}, client_errc::incomplete_message, ""}, @@ -89,7 +90,7 @@ BOOST_AUTO_TEST_CASE(success) BOOST_TEST_CONTEXT(fns.name) { test_connection conn; - conn.stream().add_message(create_ok_packet_message(1, 2, 3, 4, 5)); + conn.stream().add_message(ok_msg_builder().seqnum(1).build_ok()); // Call the function fns.ping(conn).validate_no_error(); diff --git a/test/unit/detail/network_algorithms/query.cpp b/test/unit/detail/network_algorithms/query.cpp deleted file mode 100644 index af17dd11..00000000 --- a/test/unit/detail/network_algorithms/query.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "assert_buffer_equals.hpp" -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "printing.hpp" -#include "run_coroutine.hpp" -#include "test_common.hpp" -#include "test_connection.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::blob; -using boost::mysql::column_type; -using boost::mysql::common_server_errc; -using boost::mysql::error_code; -using boost::mysql::results; -using boost::mysql::string_view; -using boost::mysql::detail::connection_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::results_access; -using boost::mysql::detail::resultset_encoding; -using namespace boost::mysql::test; - -namespace { - -using netfun_maker = netfun_maker_mem; - -struct -{ - netfun_maker::signature query; - const char* name; -} all_fns[] = { - {netfun_maker::sync_errc(&test_connection::query), "sync_errc" }, - {netfun_maker::sync_exc(&test_connection::query), "sync_exc" }, - {netfun_maker::async_errinfo(&test_connection::async_query), "async_errinfo" }, - {netfun_maker::async_noerrinfo(&test_connection::async_query), "async_noerrinfo"}, -}; - -// Verify that we reset the results object -results create_initial_results() -{ - results res; - results_access::get_rows(res) = makerows(1, 42, "abc"); - results_access::get_state(res - ) = create_execution_state(resultset_encoding::binary, {protocol_field_type::geometry}, 4); - return res; -} - -BOOST_AUTO_TEST_SUITE(test_query) - -BOOST_AUTO_TEST_CASE(success) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - conn.stream().add_message(create_ok_packet_message(1, 2, 3, 4, 5, "info")); - - // Call the function - fns.query(conn, "SELECT 1", result).validate_no_error(); - - // Verify the message we sent - std::uint8_t expected_message[] = { - 0x09, - 0x00, - 0x00, - 0x00, - 0x03, - 0x53, - 0x45, - 0x4c, - 0x45, - 0x43, - 0x54, - 0x20, - 0x31, - }; - BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), expected_message); - - // Verify the results - BOOST_TEST(result.meta().size() == 0u); - BOOST_TEST(result.affected_rows() == 2u); - BOOST_TEST(result.last_insert_id() == 3u); - BOOST_TEST(result.warning_count() == 5u); - BOOST_TEST(result.info() == "info"); - } - } -} - -BOOST_AUTO_TEST_CASE(error_start_query) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - conn.stream().set_fail_count(fail_count(0, common_server_errc::er_aborting_connection)); - - // Call the function - fns.query(conn, "SELECT 1", result) - .validate_error_exact(common_server_errc::er_aborting_connection); - } - } -} - -BOOST_AUTO_TEST_CASE(error_read_all_rows) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto result = create_initial_results(); - test_connection conn; - conn.stream().add_message(create_message(1, {0x01})); // Response OK, 1 metadata packet - conn.stream().add_message(create_coldef_message(2, protocol_field_type::geometry)); - conn.stream().set_fail_count(fail_count(4, common_server_errc::er_aborting_connection)); - - // Call the function - fns.query(conn, "SELECT 1", result) - .validate_error_exact(common_server_errc::er_aborting_connection); - - // Ensure we successfully ran the start_query - BOOST_TEST_REQUIRE(results_access::get_state(result).meta().size() == 1u); - BOOST_TEST(results_access::get_state(result).meta()[0].type() == column_type::geometry); - } - } -} - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/read_all_rows.cpp b/test/unit/detail/network_algorithms/read_all_rows.cpp deleted file mode 100644 index e8fe5ba2..00000000 --- a/test/unit/detail/network_algorithms/read_all_rows.cpp +++ /dev/null @@ -1,272 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include - -#include -#include -#include - -#include -#include - -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "test_channel.hpp" -#include "test_common.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::client_errc; -using boost::mysql::error_code; -using boost::mysql::execution_state; -using boost::mysql::rows; -using boost::mysql::detail::async_read_all_rows; -using boost::mysql::detail::execution_state_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::read_all_rows; -using boost::mysql::detail::resultset_encoding; -using namespace boost::mysql::test; - -namespace { - -using netfun_maker = netfun_maker_fn; - -struct -{ - typename netfun_maker::signature read_all_rows; - const char* name; -} all_fns[] = { - {netfun_maker::sync_errc(&read_all_rows), "sync" }, - {netfun_maker::async_errinfo(&async_read_all_rows), "async"} -}; - -// Verify that we clear any previous result -rows make_initial_rows() { return makerows(2, 42, nullptr, 4.5f, "abc"); } - -BOOST_AUTO_TEST_SUITE(test_read_all_rows) - -BOOST_AUTO_TEST_CASE(success_row_row_eof) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto row2 = create_message(5, {0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}); - auto ok_packet = create_message(6, {0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); - auto chan = create_channel(concat_copy(row1, row2, ok_packet), 1024); - chan.shared_fields().emplace_back("abc"); // from previous call - rows rws = make_initial_rows(); - - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST_REQUIRE(rws.size() == 2u); - BOOST_TEST(rws[0] == makerow("min", 1901)); - BOOST_TEST(rws[1] == makerow("max", nullptr)); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(chan.shared_sequence_number() == 0u); // not used - } - } -} - -BOOST_AUTO_TEST_CASE(success_row_row_eof_separate) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto row2 = create_message(5, {0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}); - auto ok_packet = create_message(6, {0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); - auto chan = create_channel({}, 1024); - chan.lowest_layer().add_message(row1); - chan.lowest_layer().add_message(concat_copy(row2, ok_packet)); - chan.shared_fields().emplace_back("abc"); // from previous call - rows rws = make_initial_rows(); - - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST_REQUIRE(rws.size() == 2u); - BOOST_TEST(rws[0] == makerow("min", 1901)); - BOOST_TEST(rws[1] == makerow("max", nullptr)); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(chan.shared_sequence_number() == 0u); // not used - } - } -} - -BOOST_AUTO_TEST_CASE(success_row_eof_separate) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto ok_packet = create_message(5, {0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); - auto chan = create_channel({}, 1024); - chan.lowest_layer().add_message(row1); - chan.lowest_layer().add_message(ok_packet); - chan.shared_fields().emplace_back("abc"); // from previous call - rows rws = make_initial_rows(); - - // row - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST_REQUIRE(rws.size() == 1u); - BOOST_TEST(rws[0] == makerow("min", 1901)); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(chan.shared_sequence_number() == 0u); // not used - } - } -} - -BOOST_AUTO_TEST_CASE(success_eof) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto ok_packet = create_message(4, {0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); - auto chan = create_channel(ok_packet, 1024); - chan.shared_fields().emplace_back("abc"); // from previous call - rows rws = make_initial_rows(); - - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST(rws.size() == 0u); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(chan.shared_sequence_number() == 0u); // not used - } - } -} - -// caught as failing by an integ test -BOOST_AUTO_TEST_CASE(success_eof_shared_fields_empty) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto ok_packet = create_message(4, {0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); - auto chan = create_channel(ok_packet, 1024); - rows rws = make_initial_rows(); - - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST(rws.size() == 0u); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(chan.shared_sequence_number() == 0u); // not used - } - } -} - -BOOST_AUTO_TEST_CASE(resultset_already_complete) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto st = create_execution_state(resultset_encoding::text, {}); - execution_state_access::complete(st, boost::mysql::detail::ok_packet{}); - test_channel chan = create_channel(); - rows rws = make_initial_rows(); - - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST(rws.empty()); - BOOST_TEST(st.complete()); - - // Doing it again works, too - fns.read_all_rows(chan, st, rws).validate_no_error(); - BOOST_TEST(rws.empty()); - BOOST_TEST(st.complete()); - } - } -} - -BOOST_AUTO_TEST_CASE(error_reading_row) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto st = create_execution_state(resultset_encoding::text, {}); - test_channel chan = create_channel(); - rows rws = make_initial_rows(); - chan.lowest_layer().set_fail_count(fail_count(0, client_errc::server_unsupported)); - - fns.read_all_rows(chan, st, rws).validate_error_exact(client_errc::server_unsupported); - BOOST_TEST(rws.empty()); - BOOST_TEST(!st.complete()); - } - } -} - -BOOST_AUTO_TEST_CASE(error_deserializing_row) -{ - for (const auto& fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - auto r = create_message(0, {0x00}); // invalid row - auto st = create_execution_state(resultset_encoding::binary, {protocol_field_type::var_string}); - test_channel chan = create_channel(); - rows rws = make_initial_rows(); - chan.lowest_layer().add_message(r); - - // deserialize row error - fns.read_all_rows(chan, st, rws).validate_error_exact(client_errc::incomplete_message); - BOOST_TEST(rws.empty()); - BOOST_TEST(!st.complete()); - } - } -} - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/read_resultset_head.cpp b/test/unit/detail/network_algorithms/read_resultset_head.cpp new file mode 100644 index 00000000..7bdac835 --- /dev/null +++ b/test/unit/detail/network_algorithms/read_resultset_head.cpp @@ -0,0 +1,310 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "creation/create_message_struct.hpp" +#include "test_channel.hpp" +#include "test_common.hpp" +#include "test_connection.hpp" +#include "unit_netfun_maker.hpp" + +using namespace boost::mysql::test; + +using boost::mysql::client_errc; +using boost::mysql::column_type; +using boost::mysql::common_server_errc; +using boost::mysql::execution_state; +using boost::mysql::field_view; +using boost::mysql::detail::protocol_field_type; +using boost::mysql::detail::resultset_encoding; + +namespace { + +using netfun_maker = netfun_maker_mem; + +struct +{ + typename netfun_maker::signature read_resultset_head; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&test_connection::read_resultset_head), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::read_resultset_head), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_read_resultset_head), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_read_resultset_head), "async_noerrinfo"}, +}; + +BOOST_AUTO_TEST_SUITE(test_read_resultset_head) + +struct fixture +{ + // We initiate seqnum to 1 because the initial request will have advanced it to this value + std::vector fields; + execution_state st{exec_builder(false).reset(resultset_encoding::text, &fields).seqnum(1).build_state()}; + test_connection conn; + + fixture() + { + chan().shared_sequence_number() = 42u; + conn.set_meta_mode(boost::mysql::metadata_mode::full); + } + + test_channel& chan() { return get_channel(conn); } + boost::mysql::detail::execution_state_impl& st_impl() + { + return boost::mysql::detail::execution_state_access::get_impl(st); + } +}; + +BOOST_AUTO_TEST_CASE(success_one_meta) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_message(1, {0x01}); + auto col = create_coldef_message(2, protocol_field_type::var_string); + fix.chan().lowest_layer().add_message(concat_copy(response, col)); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // We've read the response + BOOST_TEST(fix.st.should_read_rows()); + BOOST_TEST(fix.st_impl().sequence_number() == 3u); + check_meta(fix.st.meta(), {std::make_pair(column_type::varchar, "mycol")}); + } + } +} + +BOOST_AUTO_TEST_CASE(success_one_meta_metadata_minimal) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_message(1, {0x01}); + auto col = create_coldef_message(2, protocol_field_type::var_string); + fix.chan().lowest_layer().add_message(concat_copy(response, col)); + fix.chan().set_meta_mode(boost::mysql::metadata_mode::minimal); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // We've read the response + BOOST_TEST(fix.st.should_read_rows()); + BOOST_TEST(fix.st_impl().sequence_number() == 3u); + check_meta(fix.st.meta(), {std::make_pair(column_type::varchar, "")}); + } + } +} + +BOOST_AUTO_TEST_CASE(success_several_meta_separate) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_message(1, {0x02}); + auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); + auto col2 = create_coldef_message(3, protocol_field_type::tiny, "f2"); + fix.chan().lowest_layer().add_message(response); + fix.chan().lowest_layer().add_message(col1); + fix.chan().lowest_layer().add_message(col2); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // We've read the response + BOOST_TEST(fix.st.should_read_rows()); + BOOST_TEST(fix.st_impl().sequence_number() == 4u); + check_meta( + fix.st.meta(), + { + std::make_pair(column_type::varchar, "f1"), + std::make_pair(column_type::tinyint, "f2"), + } + ); + } + } +} + +BOOST_AUTO_TEST_CASE(success_ok_packet) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = ok_msg_builder().seqnum(1).affected_rows(42).info("abc").build_ok(); + fix.chan().lowest_layer().add_message(response); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // We've read the response + BOOST_TEST(fix.st.meta().size() == 0u); + BOOST_TEST_REQUIRE(fix.st.complete()); + BOOST_TEST(fix.st.affected_rows() == 42u); + BOOST_TEST(fix.st.info() == "abc"); + } + } +} + +// Should be a no-op +BOOST_AUTO_TEST_CASE(state_complete) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + fix.st_impl().on_head_ok_packet(ok_builder().affected_rows(42).build()); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // Nothing changed + BOOST_TEST_REQUIRE(fix.st.complete()); + BOOST_TEST(fix.st.affected_rows() == 42u); + } + } +} + +// Should be a no-op +BOOST_AUTO_TEST_CASE(state_reading_rows) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + fix.st_impl().on_num_meta(1); + fix.st_impl().on_meta( + create_coldef(protocol_field_type::bit), + boost::mysql::metadata_mode::minimal + ); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_no_error(); + + // Nothing changed + BOOST_TEST_REQUIRE(fix.st.should_read_rows()); + check_meta(fix.st.meta(), {column_type::bit}); + } + } +} + +BOOST_AUTO_TEST_CASE(error_network_error) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + // This covers: error writing the request, error reading + // the initial response, error reading successive metadata packets + for (std::size_t i = 0; i <= 2; ++i) + { + BOOST_TEST_CONTEXT(i) + { + fixture fix; + auto response = create_message(1, {0x02}); + auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); + auto col2 = create_coldef_message(3, protocol_field_type::tiny, "f2"); + fix.chan().lowest_layer().add_message(response); + fix.chan().lowest_layer().add_message(col1); + fix.chan().lowest_layer().add_message(col2); + fix.chan().lowest_layer().set_fail_count(fail_count(i, client_errc::server_unsupported)); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st) + .validate_error_exact(client_errc::server_unsupported); + } + } + } + } +} + +BOOST_AUTO_TEST_CASE(error_metadata_packets_seqnum_mismatch) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_message(1, {0x02}); + auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); + auto col2 = create_coldef_message(4, protocol_field_type::tiny, "f2"); + fix.chan().lowest_layer().add_message(response); + fix.chan().lowest_layer().add_message(col1); + fix.chan().lowest_layer().add_message(col2); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st) + .validate_error_exact(client_errc::sequence_number_mismatch); + } + } +} + +// All cases where the deserialization of the execution_response +// yields an error are handled uniformly, so it's enough with this test +BOOST_AUTO_TEST_CASE(error_deserialize_execution_response) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_err_packet_message(1, common_server_errc::er_bad_db_error, "no_db"); + fix.chan().lowest_layer().add_message(response); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st) + .validate_error_exact(common_server_errc::er_bad_db_error, "no_db"); + } + } +} + +BOOST_AUTO_TEST_CASE(error_deserialize_metadata) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + fixture fix; + auto response = create_message(1, {0x01}); + auto col = create_message(2, {0x08, 0x03}); + fix.chan().lowest_layer().add_message(response); + fix.chan().lowest_layer().add_message(col); + + // Call the function + fns.read_resultset_head(fix.conn, fix.st).validate_error_exact(client_errc::incomplete_message); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/read_some_rows.cpp b/test/unit/detail/network_algorithms/read_some_rows.cpp index c451976b..18a29dc1 100644 --- a/test/unit/detail/network_algorithms/read_some_rows.cpp +++ b/test/unit/detail/network_algorithms/read_some_rows.cpp @@ -6,38 +6,26 @@ // #include -#include -#include -#include +#include #include -#include #include -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "test_channel.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "creation/create_message_struct.hpp" +#include "creation/create_row_message.hpp" #include "test_common.hpp" #include "test_connection.hpp" #include "unit_netfun_maker.hpp" -using boost::mysql::client_errc; -using boost::mysql::error_code; -using boost::mysql::execution_state; -using boost::mysql::rows_view; -using boost::mysql::detail::execution_state_access; using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::resultset_encoding; using namespace boost::mysql::test; +using namespace boost::mysql; namespace { -test_channel& get_channel(test_connection& conn) noexcept -{ - return boost::mysql::detail::connection_access::get_channel(conn); -} - using netfun_maker = netfun_maker_mem; struct @@ -45,200 +33,217 @@ struct typename netfun_maker::signature read_some_rows; const char* name; } all_fns[] = { - {netfun_maker::sync_errc(&test_connection::read_some_rows), "sync" }, - {netfun_maker::async_errinfo(&test_connection::async_read_some_rows), "async"}, + {netfun_maker::sync_errc(&test_connection::read_some_rows), "sync_errc" }, + {netfun_maker::sync_exc(&test_connection::read_some_rows), "sync_exc" }, + {netfun_maker::async_errinfo(&test_connection::async_read_some_rows), "async_errinfo" }, + {netfun_maker::async_noerrinfo(&test_connection::async_read_some_rows), "async_noerrinfo"}, }; +execution_state create_initial_state(test_connection& conn) +{ + return exec_builder(false) + .reset(detail::resultset_encoding::text, &get_channel(conn).shared_fields()) + .meta({protocol_field_type::var_string}) + .rows(makerows(1, 42, 50)) + .seqnum(42) + .build_state(); +} + BOOST_AUTO_TEST_SUITE(test_read_some_rows) -BOOST_AUTO_TEST_CASE(success_row_row_eof) +BOOST_AUTO_TEST_CASE(empty_resultset) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto row2 = create_message(5, {0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}); - auto ok_packet = create_eof_packet_message(6, 1, 6, 0, 9, "ab"); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); test_connection conn; - get_channel(conn).shared_fields().emplace_back("abc"); // from previous call - conn.stream().add_message(concat_copy(row1, row2, ok_packet)); + auto st = create_initial_state(conn); + get_channel(conn).lowest_layer().add_message( + ok_msg_builder().affected_rows(1).info("1st").seqnum(42).build_eof() + ); rows_view rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST_REQUIRE(rv.size() == 2u); - BOOST_TEST(rv[0] == makerow("min", 1901)); - BOOST_TEST(rv[1] == makerow("max", nullptr)); - BOOST_TEST(st.complete()); + BOOST_TEST(rv == makerows(1)); + BOOST_TEST_REQUIRE(st.complete()); BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); + BOOST_TEST(st.info() == "1st"); BOOST_TEST(get_channel(conn).shared_sequence_number() == 0u); // not used } } } -BOOST_AUTO_TEST_CASE(success_row_row_eof_separate) +BOOST_AUTO_TEST_CASE(batch_with_rows) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto row2 = create_message(5, {0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}); - auto ok_packet = create_eof_packet_message(6, 1, 6, 0, 9, "ab"); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); test_connection conn; - get_channel(conn).shared_fields().emplace_back("abc"); // from previous call - conn.stream().add_message(row1); - conn.stream().add_message(concat_copy(row2, ok_packet)); + auto st = create_initial_state(conn); + get_channel(conn) + .lowest_layer() + .add_message( + concat_copy(create_text_row_message(42, "abc"), create_text_row_message(43, "von")) + ) + .add_message(create_text_row_message(44, "other")); // only a single read should be issued - // 1st read rows_view rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST_REQUIRE(rv.size() == 1u); - BOOST_TEST(rv[0] == makerow("min", 1901)); - BOOST_TEST(!st.complete()); - - // 2nd read - rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST_REQUIRE(rv.size() == 1u); - BOOST_TEST(rv[0] == makerow("max", nullptr)); - BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); + BOOST_TEST(rv == makerows(1, "abc", "von")); + BOOST_TEST(st.should_read_rows()); BOOST_TEST(get_channel(conn).shared_sequence_number() == 0u); // not used } } } -BOOST_AUTO_TEST_CASE(success_row_eof_separate) +BOOST_AUTO_TEST_CASE(batch_with_rows_eof) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto row1 = create_message(4, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}); - auto ok_packet = create_eof_packet_message(5, 1, 6, 0, 9, "ab"); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); test_connection conn; - get_channel(conn).shared_fields().emplace_back("abc"); // from previous call - conn.stream().add_message(row1); - conn.stream().add_message(ok_packet); + auto st = create_initial_state(conn); + get_channel(conn).lowest_layer().add_message(concat_copy( + create_text_row_message(42, "abc"), + create_text_row_message(43, "von"), + ok_msg_builder().seqnum(44).affected_rows(1).info("1st").build_eof() + )); - // row rows_view rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST_REQUIRE(rv.size() == 1u); - BOOST_TEST(rv[0] == makerow("min", 1901)); - BOOST_TEST(!st.complete()); - - // eof - rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST(rv.size() == 0u); - BOOST_TEST(st.complete()); + BOOST_TEST(rv == makerows(1, "abc", "von")); + BOOST_TEST_REQUIRE(st.complete()); BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); + BOOST_TEST(st.info() == "1st"); BOOST_TEST(get_channel(conn).shared_sequence_number() == 0u); // not used } } } -BOOST_AUTO_TEST_CASE(success_eof) +// Regression check: don't attempt to continue reading after the 1st EOF for multi-result +BOOST_AUTO_TEST_CASE(batch_with_rows_eof_multiresult) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto ok_packet = create_eof_packet_message(4, 1, 6, 0, 9, "ab"); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_}, - 4 // seqnum - ); test_connection conn; - get_channel(conn).shared_fields().emplace_back("abc"); // from previous call - conn.stream().add_message(ok_packet); + auto st = create_initial_state(conn); + get_channel(conn).lowest_layer().add_message(concat_copy( + create_text_row_message(42, "abc"), + ok_msg_builder().seqnum(43).affected_rows(1).info("1st").more_results(true).build_eof(), + ok_msg_builder().seqnum(44).info("2nd").build_ok() + )); rows_view rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST(rv.size() == 0u); - BOOST_TEST(st.complete()); + BOOST_TEST(rv == makerows(1, "abc")); + BOOST_TEST_REQUIRE(st.should_read_head()); BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(get_channel(conn).shared_sequence_number() == 0u); // not used + BOOST_TEST(st.info() == "1st"); } } } -BOOST_AUTO_TEST_CASE(resultset_already_complete) +// read_some_rows is a no-op if !st.should_read_rows() +BOOST_AUTO_TEST_CASE(state_complete) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto st = create_execution_state(resultset_encoding::text, {}); - execution_state_access::complete(st, boost::mysql::detail::ok_packet{}); test_connection conn; + auto st = exec_builder(false) + .reset(&get_channel(conn).shared_fields()) + .meta({protocol_field_type::var_string}) + .rows(makerows(1, 60, 70)) + .ok(ok_builder().affected_rows(90).info("1st").build()) + .seqnum(42) + .build_state(); rows_view rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST(rv.empty()); - BOOST_TEST(st.complete()); - - // Doing it again works, too - rv = fns.read_some_rows(conn, st).get(); - BOOST_TEST(rv.empty()); - BOOST_TEST(st.complete()); + BOOST_TEST(rv == rows()); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(st.affected_rows() == 90u); + BOOST_TEST(st.info() == "1st"); } } } -BOOST_AUTO_TEST_CASE(error_reading_row) +BOOST_AUTO_TEST_CASE(state_reading_head) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto st = create_execution_state(resultset_encoding::text, {}); test_connection conn; - conn.stream().set_fail_count(fail_count(0, client_errc::server_unsupported)); + auto st = exec_builder(false) + .reset(&get_channel(conn).shared_fields()) + .meta({protocol_field_type::var_string}) + .rows(makerows(1, 60, 70)) + .ok(ok_builder().affected_rows(90).info("1st").more_results(true).build()) + .seqnum(42) + .build_state(); - fns.read_some_rows(conn, st).validate_error_exact(client_errc::server_unsupported); - BOOST_TEST(!st.complete()); + rows_view rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST(rv == rows()); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(st.affected_rows() == 90u); + BOOST_TEST(st.info() == "1st"); } } } -BOOST_AUTO_TEST_CASE(error_deserializing_row) +BOOST_AUTO_TEST_CASE(error_network_error) { for (const auto& fns : all_fns) { BOOST_TEST_CONTEXT(fns.name) { - auto r = create_message(0, {0x00}); // invalid row - auto st = create_execution_state(resultset_encoding::binary, {protocol_field_type::var_string}); - test_connection conn; - conn.stream().add_message(r); + for (std::size_t i = 0; i <= 1; ++i) + { + BOOST_TEST_CONTEXT("i=" << i) + { + test_connection conn; + auto st = create_initial_state(conn); + get_channel(conn) + .lowest_layer() + .add_message(create_text_row_message(42, "abc")) + .add_message(ok_msg_builder().seqnum(43).affected_rows(1).info("1st").build_eof()) + .set_fail_count(fail_count(i, client_errc::wrong_num_params)); + + fns.read_some_rows(conn, st).validate_error_exact(client_errc::wrong_num_params); + } + } + } + } +} + +BOOST_AUTO_TEST_CASE(error_seqnum_mismatch) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + test_connection conn; + auto st = create_initial_state(conn); + get_channel(conn).lowest_layer().add_message(create_text_row_message(0, "abc")); + + fns.read_some_rows(conn, st).validate_error_exact(client_errc::sequence_number_mismatch); + } + } +} + +BOOST_AUTO_TEST_CASE(error_deserialize_row) +{ + for (const auto& fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + test_connection conn; + auto st = create_initial_state(conn); + get_channel(conn).lowest_layer().add_message(create_message(42, {0x02, 0xff})); - // deserialize row error fns.read_some_rows(conn, st).validate_error_exact(client_errc::incomplete_message); - BOOST_TEST(!st.complete()); } } } diff --git a/test/unit/detail/network_algorithms/start_execution.cpp b/test/unit/detail/network_algorithms/start_execution.cpp new file mode 100644 index 00000000..27b1811e --- /dev/null +++ b/test/unit/detail/network_algorithms/start_execution.cpp @@ -0,0 +1,133 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "assert_buffer_equals.hpp" +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "printing.hpp" +#include "test_channel.hpp" +#include "test_common.hpp" +#include "unit_netfun_maker.hpp" + +using boost::mysql::blob; +using boost::mysql::client_errc; +using boost::mysql::column_type; +using boost::mysql::error_code; +using boost::mysql::execution_state; +using boost::mysql::field_view; +using boost::mysql::detail::protocol_field_type; +using boost::mysql::detail::resultset_encoding; +using namespace boost::mysql::test; + +namespace { + +using netfun_maker = netfun_maker_fn; + +struct +{ + typename netfun_maker::signature start_execution; + const char* name; +} all_fns[] = { + {netfun_maker::sync_errc(&boost::mysql::detail::start_execution), "sync" }, + {netfun_maker::async_errinfo(&boost::mysql::detail::async_start_execution), "async"} +}; + +BOOST_AUTO_TEST_SUITE(test_start_execution) + +BOOST_AUTO_TEST_CASE(success) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + // Initial state, to verify that we reset it + std::vector fields; + auto st = exec_builder(false) + .reset(resultset_encoding::text, &fields) + .meta({protocol_field_type::geometry}) + .seqnum(4) + .build_state(); + + // Channel + auto chan = create_channel(); + chan.lowest_layer() + .add_message(create_message(1, {0x01})) + .add_message(create_coldef_message(2, protocol_field_type::var_string)); + chan.shared_sequence_number() = 42u; + + // Get an execution request into the channel's buffer + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); + + // Call the function + fns.start_execution(chan, resultset_encoding::binary, st).validate_no_error(); + + // We've written the request message + auto expected_msg = create_message(0, {0x02, 0x05, 0x09}); + BOOST_MYSQL_ASSERT_BLOB_EQUALS(chan.lowest_layer().bytes_written(), expected_msg); + BOOST_TEST(chan.shared_sequence_number() == 42u); // unused + + // We've read the response + BOOST_TEST(get_impl(st).encoding() == resultset_encoding::binary); + BOOST_TEST(get_impl(st).sequence_number() == 3u); + BOOST_TEST(st.should_read_rows()); + check_meta( + get_impl(st).current_resultset_meta(), + {std::make_pair(column_type::varchar, "mycol")} + ); + } + } +} + +// This covers errors in both writing the request and calling read_resultset_head +BOOST_AUTO_TEST_CASE(error_network_error) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + for (std::size_t i = 0; i <= 1; ++i) + { + BOOST_TEST_CONTEXT(i) + { + std::vector fields; + auto st = exec_builder(false).reset(&fields).build_state(); + auto chan = create_channel(); + chan.lowest_layer() + .add_message(create_message(1, {0x01})) + .add_message(create_coldef_message(2, protocol_field_type::var_string)) + .set_fail_count(fail_count(i, client_errc::server_unsupported)); + + // Get an execution request into the channel's buffer + concat(chan.shared_buffer(), {0x02, 0x05, 0x09}); + + // Call the function + fns.start_execution(chan, resultset_encoding::binary, st) + .validate_error_exact(client_errc::server_unsupported); + } + } + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/start_execution_generic.cpp b/test/unit/detail/network_algorithms/start_execution_generic.cpp deleted file mode 100644 index 1ee65c3f..00000000 --- a/test/unit/detail/network_algorithms/start_execution_generic.cpp +++ /dev/null @@ -1,424 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include - -#include "assert_buffer_equals.hpp" -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "network_result.hpp" -#include "printing.hpp" -#include "test_channel.hpp" -#include "test_common.hpp" -#include "test_stream.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::blob; -using boost::mysql::client_errc; -using boost::mysql::column_type; -using boost::mysql::common_server_errc; -using boost::mysql::diagnostics; -using boost::mysql::error_code; -using boost::mysql::execution_state; -using boost::mysql::detail::async_start_execution_generic; -using boost::mysql::detail::capabilities; -using boost::mysql::detail::db_flavor; -using boost::mysql::detail::deserialize_execute_response; -using boost::mysql::detail::execute_response; -using boost::mysql::detail::execution_state_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::resultset_encoding; -using boost::mysql::detail::start_execution_generic; -using namespace boost::mysql::test; - -BOOST_TEST_DONT_PRINT_LOG_VALUE(execute_response::type_t) - -namespace { - -using serialize_fn = std::function&)>; -using netfun_maker = netfun_maker_fn< - void, - resultset_encoding, - test_channel&, - const serialize_fn&, - execution_state&>; - -struct fns_t -{ - typename netfun_maker::signature start_execution_generic; - const char* name; -} all_fns[] = { - {netfun_maker::sync_errc(&start_execution_generic), "sync" }, - {netfun_maker::async_errinfo(&async_start_execution_generic), "async"} -}; - -// Verify that we reset the state -execution_state create_initial_state() -{ - return create_execution_state(resultset_encoding::text, {protocol_field_type::geometry}, 4); -} - -BOOST_AUTO_TEST_SUITE(test_start_execution_generic) - -BOOST_AUTO_TEST_SUITE(deserialize_execute_response_) -BOOST_AUTO_TEST_CASE(ok_packet) -{ - std::uint8_t msg[] = {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}; - diagnostics diag; - auto response = deserialize_execute_response( - boost::asio::buffer(msg), - capabilities(), - db_flavor::mariadb, - diag - ); - BOOST_TEST(response.type == execute_response::type_t::ok_packet); - BOOST_TEST(response.data.ok_pack.affected_rows.value == 0u); - BOOST_TEST(response.data.ok_pack.status_flags == 2u); -} - -BOOST_AUTO_TEST_CASE(num_fields) -{ - struct - { - const char* name; - std::vector msg; - std::size_t num_fields; - } test_cases[] = { - {"1", {0x01}, 1 }, - {"0xfa", {0xfa}, 0xfa }, - {"0xfb_no_local_infile", {0xfb}, 0xfb }, // legal when LOCAL INFILE capability not enabled - {"0xfb_local_infile", {0xfc, 0xfb, 0x00}, 0xfb }, // sent LOCAL INFILE capability is enabled - {"0xff", {0xfc, 0xff, 0x00}, 0xff }, - {"0x01ff", {0xfc, 0x00, 0x01}, 0x01ff}, - {"max", {0xfc, 0xff, 0xff}, 0xffff}, - }; - - for (const auto& tc : test_cases) - { - BOOST_TEST_CONTEXT(tc.name) - { - std::uint8_t msg[] = {0xfc, 0xff, 0x00}; - diagnostics diag; - auto response = deserialize_execute_response( - boost::asio::buffer(msg), - capabilities(), - db_flavor::mysql, - diag - ); - BOOST_TEST(response.type == execute_response::type_t::num_fields); - BOOST_TEST(response.data.num_fields == 0xffu); - BOOST_TEST(diag.server_message() == ""); - } - } -} - -BOOST_AUTO_TEST_CASE(error) -{ - struct - { - const char* name; - std::vector msg; - error_code err; - const char* expected_info; - } test_cases[] = { - {"server_error", - {0xff, 0x7a, 0x04, 0x23, 0x34, 0x32, 0x53, 0x30, 0x32, 0x54, 0x61, 0x62, 0x6c, 0x65, - 0x20, 0x27, 0x6d, 0x79, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x61, 0x62, 0x63, 0x27, 0x20, - 0x64, 0x6f, 0x65, 0x73, 0x6e, 0x27, 0x74, 0x20, 0x65, 0x78, 0x69, 0x73, 0x74}, - common_server_errc::er_no_such_table, - "Table 'mytest.abc' doesn't exist" }, - {"bad_server_error", {0xff, 0x00}, client_errc::incomplete_message, ""}, - {"bad_ok_packet", {0x00, 0xff}, client_errc::incomplete_message, ""}, - {"bad_num_fields", {0xfc, 0xff, 0x00, 0x01}, client_errc::extra_bytes, ""}, - {"zero_num_fields", {0xfc, 0x00, 0x00}, client_errc::protocol_value_error, ""}, - {"3byte_integer", {0xfd, 0xff, 0xff, 0xff}, client_errc::protocol_value_error, ""}, - {"8byte_integer", - {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, - client_errc::protocol_value_error, - "" }, - }; - - for (const auto& tc : test_cases) - { - BOOST_TEST_CONTEXT(tc.name) - { - diagnostics diag; - auto response = deserialize_execute_response( - boost::asio::buffer(tc.msg), - capabilities(), - db_flavor::mysql, - diag - ); - BOOST_TEST(response.type == execute_response::type_t::error); - BOOST_TEST(response.data.err == tc.err); - BOOST_TEST(diag.server_message() == tc.expected_info); - } - } -} -BOOST_AUTO_TEST_SUITE_END() - -struct fixture -{ - execution_state st{create_initial_state()}; - serialize_fn serializer{[](capabilities, std::vector& buff) { - std::uint8_t message[] = {0x01, 0x02, 0x03}; - buff.assign(std::begin(message), std::end(message)); - }}; - test_channel chan{create_channel()}; - - fixture() - { - chan.set_meta_mode(boost::mysql::metadata_mode::full); - chan.shared_sequence_number() = 42; - } - - void check_written_message() - { - BOOST_MYSQL_ASSERT_BLOB_EQUALS( - chan.lowest_layer().bytes_written(), - blob({0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03}) - ); - BOOST_TEST(chan.shared_sequence_number() == 42u); // not used - } - - network_result call_fn(const fns_t& fns) - { - return fns.start_execution_generic(resultset_encoding::binary, chan, serializer, st); - } -}; - -BOOST_AUTO_TEST_CASE(success_one_meta) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_message(1, {0x01}); - auto col = create_coldef_message(2, protocol_field_type::var_string); - fix.chan.lowest_layer().add_message(concat_copy(response, col)); - - // Call the function - fix.call_fn(fns).validate_no_error(); - - // We've written the request message - fix.check_written_message(); - - // We've read the response - BOOST_TEST(execution_state_access::get_encoding(fix.st) == resultset_encoding::binary); - BOOST_TEST(!fix.st.complete()); - BOOST_TEST(execution_state_access::get_sequence_number(fix.st) == 3u); - BOOST_TEST_REQUIRE(fix.st.meta().size() == 1u); - BOOST_TEST(fix.st.meta()[0].column_name() == "mycol"); - BOOST_TEST(fix.st.meta()[0].type() == column_type::varchar); - } - } -} - -BOOST_AUTO_TEST_CASE(success_one_meta_metadata_minimal) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_message(1, {0x01}); - auto col = create_coldef_message(2, protocol_field_type::var_string); - fix.chan.lowest_layer().add_message(concat_copy(response, col)); - fix.chan.set_meta_mode(boost::mysql::metadata_mode::minimal); - - // Call the function - fix.call_fn(fns).validate_no_error(); - - // We've written the request message - fix.check_written_message(); - - // We've read the response - BOOST_TEST(execution_state_access::get_encoding(fix.st) == resultset_encoding::binary); - BOOST_TEST(!fix.st.complete()); - BOOST_TEST(execution_state_access::get_sequence_number(fix.st) == 3u); - BOOST_TEST_REQUIRE(fix.st.meta().size() == 1u); - BOOST_TEST(fix.st.meta()[0].column_name() == ""); - BOOST_TEST(fix.st.meta()[0].type() == column_type::varchar); - } - } -} - -BOOST_AUTO_TEST_CASE(success_several_meta_separate) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_message(1, {0x02}); - auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); - auto col2 = create_coldef_message(3, protocol_field_type::tiny, "f2"); - fix.chan.lowest_layer().add_message(response); - fix.chan.lowest_layer().add_message(col1); - fix.chan.lowest_layer().add_message(col2); - - // Call the function - fix.call_fn(fns).validate_no_error(); - - // We've written the request message - fix.check_written_message(); - - // We've read the response - BOOST_TEST(execution_state_access::get_encoding(fix.st) == resultset_encoding::binary); - BOOST_TEST(!fix.st.complete()); - BOOST_TEST(execution_state_access::get_sequence_number(fix.st) == 4u); - BOOST_TEST_REQUIRE(fix.st.meta().size() == 2u); - BOOST_TEST(fix.st.meta()[0].column_name() == "f1"); - BOOST_TEST(fix.st.meta()[0].type() == column_type::varchar); - BOOST_TEST(fix.st.meta()[1].column_name() == "f2"); - BOOST_TEST(fix.st.meta()[1].type() == column_type::tinyint); - } - } -} - -BOOST_AUTO_TEST_CASE(success_ok_packet) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_ok_packet_message(1, 42, 43, 44, 45, "abc"); - fix.chan.lowest_layer().add_message(response); - - // Call the function - fix.call_fn(fns).validate_no_error(); - - // We've written the request message - fix.check_written_message(); - - // We've read the response - BOOST_TEST(execution_state_access::get_encoding(fix.st) == resultset_encoding::binary); - BOOST_TEST(fix.st.meta().size() == 0u); - BOOST_TEST_REQUIRE(fix.st.complete()); - BOOST_TEST(fix.st.affected_rows() == 42u); - BOOST_TEST(fix.st.last_insert_id() == 43u); - BOOST_TEST(fix.st.warning_count() == 45u); - BOOST_TEST(fix.st.info() == "abc"); - } - } -} - -BOOST_AUTO_TEST_CASE(error_network_error) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - // This covers: error writing the request, error reading - // the initial response, error reading successive metadata packets - for (std::size_t i = 0; i <= 2; ++i) - { - BOOST_TEST_CONTEXT(i) - { - fixture fix; - auto response = create_message(1, {0x02}); - auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); - auto col2 = create_coldef_message(3, protocol_field_type::tiny, "f2"); - fix.chan.lowest_layer().add_message(response); - fix.chan.lowest_layer().add_message(col1); - fix.chan.lowest_layer().add_message(col2); - fix.chan.lowest_layer().set_fail_count(fail_count(i, client_errc::server_unsupported)); - - // Call the function - fix.call_fn(fns).validate_error_exact(client_errc::server_unsupported); - } - } - } - } -} - -BOOST_AUTO_TEST_CASE(error_metadata_packets_seqnum_mismatch) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_message(1, {0x02}); - auto col1 = create_coldef_message(2, protocol_field_type::var_string, "f1"); - auto col2 = create_coldef_message(4, protocol_field_type::tiny, "f2"); - fix.chan.lowest_layer().add_message(response); - fix.chan.lowest_layer().add_message(col1); - fix.chan.lowest_layer().add_message(col2); - - // Call the function - fix.call_fn(fns).validate_error_exact(client_errc::sequence_number_mismatch); - } - } -} - -// All cases where the deserialization of the execution_response -// yields an error are handled uniformly, so it's enough with this test -BOOST_AUTO_TEST_CASE(error_deserialize_execution_response) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_err_packet_message(1, common_server_errc::er_bad_db_error, "no_db"); - fix.chan.lowest_layer().add_message(response); - - // Call the function - fix.call_fn(fns).validate_error_exact(common_server_errc::er_bad_db_error, "no_db"); - } - } -} - -BOOST_AUTO_TEST_CASE(error_deserialize_metadata) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - fixture fix; - auto response = create_message(1, {0x01}); - auto col = create_message(2, {0x08, 0x03}); - fix.chan.lowest_layer().add_message(response); - fix.chan.lowest_layer().add_message(col); - - // Call the function - fix.call_fn(fns).validate_error_exact(client_errc::incomplete_message); - } - } -} - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/start_query.cpp b/test/unit/detail/network_algorithms/start_query.cpp deleted file mode 100644 index 13dcc5bd..00000000 --- a/test/unit/detail/network_algorithms/start_query.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include - -#include - -#include - -#include "assert_buffer_equals.hpp" -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "printing.hpp" -#include "test_common.hpp" -#include "test_connection.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::blob; -using boost::mysql::common_server_errc; -using boost::mysql::error_code; -using boost::mysql::execution_state; -using boost::mysql::string_view; -using boost::mysql::detail::execution_state_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::resultset_encoding; -using namespace boost::mysql::test; - -namespace { - -using netfun_maker = netfun_maker_mem; - -struct -{ - netfun_maker::signature start_query; - const char* name; -} all_fns[] = { - {netfun_maker::sync_errc(&test_connection::start_query), "sync_errc" }, - {netfun_maker::sync_exc(&test_connection::start_query), "sync_exc" }, - {netfun_maker::async_errinfo(&test_connection::async_start_query), "async_errinfo" }, - {netfun_maker::async_noerrinfo(&test_connection::async_start_query), "async_noerrinfo"}, -}; - -// Verify that we reset the state -execution_state create_initial_state() -{ - return create_execution_state(resultset_encoding::binary, {protocol_field_type::geometry}, 4); -} - -BOOST_AUTO_TEST_SUITE(test_start_query) - -BOOST_AUTO_TEST_CASE(success) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - execution_state st{create_initial_state()}; - test_connection conn; - conn.stream().add_message(create_ok_packet_message(1, 2)); - - // Call the function - fns.start_query(conn, "SELECT 1", st).validate_no_error(); - - // Verify the message we sent - std::uint8_t expected_message[] = { - 0x09, - 0x00, - 0x00, - 0x00, - 0x03, - 0x53, - 0x45, - 0x4c, - 0x45, - 0x43, - 0x54, - 0x20, - 0x31, - }; - BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), expected_message); - - // Verify the results - BOOST_TEST(execution_state_access::get_encoding(st) == resultset_encoding::text); - BOOST_TEST(st.complete()); - BOOST_TEST(execution_state_access::get_sequence_number(st) == 2u); - BOOST_TEST(st.meta().size() == 0u); - BOOST_TEST(st.affected_rows() == 2u); - } - } -} - -BOOST_AUTO_TEST_CASE(error) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - execution_state st{create_initial_state()}; - test_connection conn; - conn.stream().set_fail_count(fail_count(0, common_server_errc::er_aborting_connection)); - - // Call the function - fns.start_query(conn, "SELECT 1", st) - .validate_error_exact(common_server_errc::er_aborting_connection); - } - } -} - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/network_algorithms/start_statement_execution.cpp b/test/unit/detail/network_algorithms/start_statement_execution.cpp deleted file mode 100644 index e70befaf..00000000 --- a/test/unit/detail/network_algorithms/start_statement_execution.cpp +++ /dev/null @@ -1,301 +0,0 @@ -// -// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include - -#include "assert_buffer_equals.hpp" -#include "create_execution_state.hpp" -#include "create_message.hpp" -#include "create_statement.hpp" -#include "printing.hpp" -#include "run_coroutine.hpp" -#include "test_common.hpp" -#include "test_connection.hpp" -#include "unit_netfun_maker.hpp" - -using boost::mysql::blob; -using boost::mysql::client_errc; -using boost::mysql::error_code; -using boost::mysql::execution_state; -using boost::mysql::field_view; -using boost::mysql::statement; -using boost::mysql::string_view; -using boost::mysql::detail::execution_state_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::resultset_encoding; -using namespace boost::mysql::test; - -namespace { - -// Machinery to treat iterator and tuple overloads the same -using netfun_maker_it = netfun_maker_mem< - void, - test_connection, - const statement&, - const field_view*, - const field_view*, - execution_state&>; - -using netfun_maker_tuple = netfun_maker_mem< - void, - test_connection, - const statement&, - const std::tuple&, - execution_state&>; - -using start_statement_execution_fn = std::function< - network_result(test_connection&, const statement&, field_view, field_view, execution_state&)>; - -start_statement_execution_fn to_common_sig(const netfun_maker_it::signature& sig) -{ - return [sig]( - test_connection& conn, - const statement& stmt, - field_view p1, - field_view p2, - execution_state& st - ) { - field_view params[] = {p1, p2}; - return sig(conn, stmt, std::begin(params), std::end(params), st); - }; -} - -start_statement_execution_fn to_common_sig(const netfun_maker_tuple::signature& sig) -{ - return [sig]( - test_connection& conn, - const statement& stmt, - field_view p1, - field_view p2, - execution_state& st - ) { return sig(conn, stmt, std::make_tuple(p1, p2), st); }; -} - -struct -{ - start_statement_execution_fn start_statement_execution; - const char* name; -} all_fns[] = { - {to_common_sig(netfun_maker_it::sync_errc(&test_connection::start_statement_execution)), "sync_errc_it"}, - {to_common_sig(netfun_maker_tuple::sync_errc(&test_connection::start_statement_execution)), - "sync_errc_tuple" }, - {to_common_sig(netfun_maker_it::sync_exc(&test_connection::start_statement_execution)), "sync_exc_it" }, - {to_common_sig(netfun_maker_tuple::sync_exc(&test_connection::start_statement_execution)), - "sync_exc_tuple" }, - {to_common_sig(netfun_maker_it::async_errinfo(&test_connection::async_start_statement_execution)), - "async_errinfo_it" }, - {to_common_sig(netfun_maker_tuple::async_errinfo(&test_connection::async_start_statement_execution)), - "async_errinfo_tuple" }, - {to_common_sig(netfun_maker_it::async_noerrinfo(&test_connection::async_start_statement_execution)), - "async_noerrinfo_it" }, - {to_common_sig(netfun_maker_tuple::async_noerrinfo(&test_connection::async_start_statement_execution)), - "async_noerrinfo_tuple" }, -}; - -// Verify that we reset the state -execution_state create_initial_state() -{ - return create_execution_state(resultset_encoding::text, {protocol_field_type::geometry}, 4); -} - -BOOST_AUTO_TEST_SUITE(test_start_statement_execution) - -BOOST_AUTO_TEST_CASE(success) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - execution_state st{create_initial_state()}; - auto ok_pack = create_ok_packet_message(1, 2); - test_connection conn; - auto stmt = create_statement(2); - conn.stream().add_message(ok_pack); - - // Call the function - fns.start_statement_execution(conn, stmt, field_view("test"), field_view(nullptr), st) - .validate_no_error(); - - // Verify the message we sent - std::uint8_t expected_message[] = { - 0x015, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, - }; - BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), expected_message); - - // Verify the results - BOOST_TEST(execution_state_access::get_encoding(st) == resultset_encoding::binary); - BOOST_TEST(st.complete()); - BOOST_TEST(execution_state_access::get_sequence_number(st) == 2u); - BOOST_TEST(st.meta().size() == 0u); - BOOST_TEST(st.affected_rows() == 2u); - } - } -} - -// This covers any case where start_execution_generic would error -BOOST_AUTO_TEST_CASE(error_start_execution_generic) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - execution_state st{create_initial_state()}; - test_connection conn; - auto stmt = create_statement(2); - conn.stream().set_fail_count(fail_count(0, client_errc::server_unsupported)); - - // Call the function - fns.start_statement_execution(conn, stmt, field_view("test"), field_view(nullptr), st) - .validate_error_exact(client_errc::server_unsupported); - } - } -} - -BOOST_AUTO_TEST_CASE(error_wrong_num_params) -{ - for (auto fns : all_fns) - { - BOOST_TEST_CONTEXT(fns.name) - { - execution_state st{create_initial_state()}; - test_connection conn; - auto stmt = create_statement(3); - - // Call the function - fns.start_statement_execution(conn, stmt, field_view("test"), field_view(nullptr), st) - .validate_error_exact(client_errc::wrong_num_params); - } - } -} - -// Verify that all param types we advertise work as expected -// (database_types tests use field_views) -BOOST_AUTO_TEST_CASE(tuple_parameter_types) -{ - execution_state st{create_initial_state()}; - test_connection conn; - auto params = std::make_tuple( - std::uint8_t(42), - std::int16_t(-1), - string_view("test"), - blob({0x00, 0x01, 0x02}), - 4.2f, - nullptr, - boost::mysql::date(2020, 1, 2) - ); - statement stmt{create_statement(std::tuple_size::value)}; - conn.stream().add_message(create_ok_packet_message(1)); - - // Execute - conn.start_statement_execution(stmt, params, st); - - // Verify - std::uint8_t expected_msg[] = { - 0x3c, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x01, - 0x08, 0x80, 0x08, 0x00, 0xfe, 0x00, 0xfc, 0x00, 0x04, 0x00, 0x06, 0x00, 0x0a, 0x00, 0x2a, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x04, 0x74, - 0x65, 0x73, 0x74, 0x03, 0x00, 0x01, 0x02, 0x66, 0x66, 0x86, 0x40, 0x04, 0xe4, 0x07, 0x01, 0x02, - }; - BOOST_MYSQL_ASSERT_BLOB_EQUALS(conn.stream().bytes_written(), expected_msg); -} - -// Verify that we correctly perform a decay-copy of the parameters, -// relevant for deferred tokens -#ifdef BOOST_ASIO_HAS_CO_AWAIT -BOOST_AUTO_TEST_SUITE(tuple_params_copying) -struct fixture -{ - execution_state st{create_initial_state()}; - test_connection conn; - statement stmt{create_statement(2)}; - - static constexpr std::uint8_t expected_msg[]{ - 0x1d, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, - 0x00, 0x08, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }; - - fixture() { conn.stream().add_message(create_ok_packet_message(1)); } -}; - -BOOST_AUTO_TEST_CASE(rvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - auto aw = fix.conn.async_start_statement_execution( - fix.stmt, - std::make_tuple(std::string("test"), 42), - fix.st, - boost::asio::use_awaitable - ); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.st.complete()); - }); -} - -BOOST_AUTO_TEST_CASE(lvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - auto tup = std::make_tuple(std::string("test"), 42); - auto aw = fix.conn.async_start_statement_execution(fix.stmt, tup, fix.st, boost::asio::use_awaitable); - tup = std::make_tuple(std::string("other"), 90); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.st.complete()); - }); -} - -BOOST_AUTO_TEST_CASE(const_lvalue) -{ - run_coroutine([]() -> boost::asio::awaitable { - fixture fix; - - // Deferred op - const auto tup = std::make_tuple(std::string("test"), 42); - auto aw = fix.conn.async_start_statement_execution(fix.stmt, tup, fix.st, boost::asio::use_awaitable); - co_await std::move(aw); - - // verify that the op had the intended effects - BOOST_MYSQL_ASSERT_BLOB_EQUALS(fix.conn.stream().bytes_written(), fix.expected_msg); - BOOST_TEST(fix.st.complete()); - }); -} - -BOOST_AUTO_TEST_SUITE_END() -#endif - -BOOST_AUTO_TEST_SUITE_END() - -} // namespace \ No newline at end of file diff --git a/test/unit/detail/protocol/deserialize_binary_field.cpp b/test/unit/detail/protocol/deserialize_binary_field.cpp index 5409fc36..dc940d95 100644 --- a/test/unit/detail/protocol/deserialize_binary_field.cpp +++ b/test/unit/detail/protocol/deserialize_binary_field.cpp @@ -21,7 +21,7 @@ #include -#include "create_meta.hpp" +#include "creation/create_meta.hpp" #include "printing.hpp" #include "test_common.hpp" @@ -654,29 +654,14 @@ std::vector make_all_samples() BOOST_DATA_TEST_CASE(test_deserialize_binary_value_ok, data::make(make_all_samples())) { - auto meta = create_meta(sample.type, sample.flags, 0, sample.collation); - field_view actual_value; + auto meta = meta_builder(sample.type).flags(sample.flags).decimals(0).collation(sample.collation).build(); const bytestring& buffer = sample.from; deserialization_context ctx(buffer.data(), buffer.data() + buffer.size(), capabilities()); - auto err = deserialize_binary_field(ctx, meta, buffer.data(), actual_value); + field_view actual_value; + auto err = deserialize_binary_field(ctx, meta, actual_value); + BOOST_TEST(err == deserialize_errc::ok); - - // Strings are representd as string view offsets. Strings are prefixed - // by their length, so they don't start at offset 0 - if (sample.expected.is_string() || sample.expected.is_blob()) - { - std::size_t expected_size = sample.expected.is_string() ? sample.expected.get_string().size() - : sample.expected.get_blob().size(); - field_view expected_offset = make_svoff_fv( - buffer.size() - expected_size, - expected_size, - sample.expected.is_blob() - ); - BOOST_TEST(actual_value == expected_offset); - field_view_access::offset_to_string_view(actual_value, buffer.data()); - } - BOOST_TEST(actual_value == sample.expected); BOOST_TEST(ctx.first() == buffer.data() + buffer.size()); // all bytes consumed } @@ -1037,13 +1022,14 @@ std::vector make_all_samples() BOOST_DATA_TEST_CASE(test_deserialize_binary_value_error, data::make(make_all_samples())) { - auto meta = create_meta(sample.type, sample.flags); - field_view actual_value; + auto meta = meta_builder(sample.type).flags(sample.flags).build(); const bytestring& buff = sample.from; deserialization_context ctx(buff.data(), buff.data() + buff.size(), capabilities()); - auto err = deserialize_binary_field(ctx, meta, buff.data(), actual_value); - auto expected = sample.expected_err; - BOOST_TEST(expected == err); + + field_view actual_value; + auto err = deserialize_binary_field(ctx, meta, actual_value); + + BOOST_TEST(err == sample.expected_err); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/detail/protocol/deserialize_execute_response.cpp b/test/unit/detail/protocol/deserialize_execute_response.cpp new file mode 100644 index 00000000..dcc77eec --- /dev/null +++ b/test/unit/detail/protocol/deserialize_execute_response.cpp @@ -0,0 +1,133 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "test_common.hpp" + +using namespace boost::mysql::test; +using boost::mysql::client_errc; +using boost::mysql::common_server_errc; +using boost::mysql::diagnostics; +using boost::mysql::error_code; +using boost::mysql::detail::capabilities; +using boost::mysql::detail::db_flavor; +using boost::mysql::detail::execute_response; + +BOOST_TEST_DONT_PRINT_LOG_VALUE(execute_response::type_t) + +namespace { + +BOOST_AUTO_TEST_SUITE(test_deserialize_execute_response) + +BOOST_AUTO_TEST_CASE(ok_packet) +{ + std::uint8_t msg[] = {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}; + diagnostics diag; + auto response = deserialize_execute_response( + boost::asio::buffer(msg), + capabilities(), + db_flavor::mariadb, + diag + ); + BOOST_TEST(response.type == execute_response::type_t::ok_packet); + BOOST_TEST(response.data.ok_pack.affected_rows.value == 0u); + BOOST_TEST(response.data.ok_pack.status_flags == 2u); +} + +BOOST_AUTO_TEST_CASE(num_fields) +{ + struct + { + const char* name; + std::vector msg; + std::size_t num_fields; + } test_cases[] = { + {"1", {0x01}, 1 }, + {"0xfa", {0xfa}, 0xfa }, + {"0xfb_no_local_infile", {0xfb}, 0xfb }, // legal when LOCAL INFILE capability not enabled + {"0xfb_local_infile", {0xfc, 0xfb, 0x00}, 0xfb }, // sent LOCAL INFILE capability is enabled + {"0xff", {0xfc, 0xff, 0x00}, 0xff }, + {"0x01ff", {0xfc, 0x00, 0x01}, 0x01ff}, + {"max", {0xfc, 0xff, 0xff}, 0xffff}, + }; + + for (const auto& tc : test_cases) + { + BOOST_TEST_CONTEXT(tc.name) + { + std::uint8_t msg[] = {0xfc, 0xff, 0x00}; + diagnostics diag; + auto response = deserialize_execute_response( + boost::asio::buffer(msg), + capabilities(), + db_flavor::mysql, + diag + ); + BOOST_TEST(response.type == execute_response::type_t::num_fields); + BOOST_TEST(response.data.num_fields == 0xffu); + BOOST_TEST(diag.server_message() == ""); + } + } +} + +BOOST_AUTO_TEST_CASE(error) +{ + struct + { + const char* name; + std::vector msg; + error_code err; + const char* expected_info; + } test_cases[] = { + {"server_error", + {0xff, 0x7a, 0x04, 0x23, 0x34, 0x32, 0x53, 0x30, 0x32, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x20, 0x27, 0x6d, 0x79, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x61, 0x62, 0x63, 0x27, 0x20, + 0x64, 0x6f, 0x65, 0x73, 0x6e, 0x27, 0x74, 0x20, 0x65, 0x78, 0x69, 0x73, 0x74}, + common_server_errc::er_no_such_table, + "Table 'mytest.abc' doesn't exist" }, + {"bad_server_error", {0xff, 0x00}, client_errc::incomplete_message, ""}, + {"bad_ok_packet", {0x00, 0xff}, client_errc::incomplete_message, ""}, + {"bad_num_fields", {0xfc, 0xff, 0x00, 0x01}, client_errc::extra_bytes, ""}, + {"zero_num_fields", {0xfc, 0x00, 0x00}, client_errc::protocol_value_error, ""}, + {"3byte_integer", {0xfd, 0xff, 0xff, 0xff}, client_errc::protocol_value_error, ""}, + {"8byte_integer", + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + client_errc::protocol_value_error, + "" }, + }; + + for (const auto& tc : test_cases) + { + BOOST_TEST_CONTEXT(tc.name) + { + diagnostics diag; + auto response = deserialize_execute_response( + boost::asio::buffer(tc.msg), + capabilities(), + db_flavor::mysql, + diag + ); + BOOST_TEST(response.type == execute_response::type_t::error); + BOOST_TEST(response.data.err == tc.err); + BOOST_TEST(diag.server_message() == tc.expected_info); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/protocol/deserialize_row.cpp b/test/unit/detail/protocol/deserialize_row.cpp index 6c0c56ee..8f2926c6 100644 --- a/test/unit/detail/protocol/deserialize_row.cpp +++ b/test/unit/detail/protocol/deserialize_row.cpp @@ -11,9 +11,9 @@ #include #include #include -#include #include #include +#include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -30,8 +31,8 @@ #include #include "buffer_concat.hpp" -#include "create_execution_state.hpp" -#include "create_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_meta.hpp" #include "test_common.hpp" using namespace boost::mysql::test; @@ -43,6 +44,7 @@ using boost::mysql::diagnostics; using boost::mysql::error_code; using boost::mysql::field_view; using boost::mysql::metadata; +using boost::mysql::metadata_collection_view; namespace { @@ -68,8 +70,7 @@ BOOST_AUTO_TEST_CASE(success) const char* name; resultset_encoding encoding; std::vector from; - std::vector expected_with_offsets; // before offset conversion - std::vector expected; // after offset conversion + std::vector expected; std::vector meta; } test_cases [] = { // Text @@ -78,7 +79,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::text, {0x01, 0x35}, make_fv_vector(std::int64_t(5)), - make_fv_vector(std::int64_t(5)), make_meta({ protocol_field_type::tiny }) }, { @@ -86,14 +86,12 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::text, {0xfb}, make_fv_vector(nullptr), - make_fv_vector(nullptr), make_meta({ protocol_field_type::tiny }) }, { "text_several_values", resultset_encoding::text, {0x03, 0x76, 0x61, 0x6c, 0x02, 0x32, 0x31, 0x03, 0x30, 0x2e, 0x30}, - make_fv_vector(make_svoff_fv(1, 3, false), std::int64_t(21), 0.0f), make_fv_vector("val", std::int64_t(21), 0.0f), make_meta({ protocol_field_type::var_string, protocol_field_type::long_, protocol_field_type::float_ }) }, @@ -101,7 +99,6 @@ BOOST_AUTO_TEST_CASE(success) "text_several_values_one_null", resultset_encoding::text, {0x03, 0x76, 0x61, 0x6c, 0xfb, 0x03, 0x76, 0x61, 0x6c}, - make_fv_vector(make_svoff_fv(1, 3, false), nullptr, make_svoff_fv(6, 3, false)), make_fv_vector("val", nullptr, "val"), make_meta({ protocol_field_type::var_string, protocol_field_type::long_, protocol_field_type::var_string }) }, @@ -110,7 +107,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::text, {0xfb, 0xfb, 0xfb}, make_fv_vector(nullptr, nullptr, nullptr), - make_fv_vector(nullptr, nullptr, nullptr), make_meta({ protocol_field_type::var_string, protocol_field_type::long_, protocol_field_type::datetime }) }, @@ -120,7 +116,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::binary, {0x00, 0x00, 0x14}, make_fv_vector(std::int64_t(20)), - make_fv_vector(std::int64_t(20)), make_meta({ protocol_field_type::tiny }) }, { @@ -128,14 +123,12 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::binary, {0x00, 0x04}, make_fv_vector(nullptr), - make_fv_vector(nullptr), make_meta({ protocol_field_type::tiny }) }, { "binary_two_values", resultset_encoding::binary, {0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}, - make_fv_vector(make_svoff_fv(3, 3, false), std::int64_t(1901)), make_fv_vector("min", std::int64_t(1901)), make_meta({ protocol_field_type::var_string, protocol_field_type::short_ }) }, @@ -143,7 +136,6 @@ BOOST_AUTO_TEST_CASE(success) "binary_one_value_one_null", resultset_encoding::binary, {0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}, - make_fv_vector(make_svoff_fv(3, 3, false), nullptr), make_fv_vector("max", nullptr), make_meta({ protocol_field_type::var_string, protocol_field_type::tiny }) }, @@ -152,7 +144,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::binary, {0x00, 0x0c}, make_fv_vector(nullptr, nullptr), - make_fv_vector(nullptr, nullptr), make_meta({ protocol_field_type::tiny, protocol_field_type::tiny }) }, { @@ -160,7 +151,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::binary, {0x00, 0xfc}, std::vector(6, field_view()), - std::vector(6, field_view()), make_meta(std::vector(6, protocol_field_type::tiny)) }, { @@ -168,7 +158,6 @@ BOOST_AUTO_TEST_CASE(success) resultset_encoding::binary, {0x00, 0xfc, 0x01}, std::vector(7, field_view()), - std::vector(7, field_view()), make_meta(std::vector(7, protocol_field_type::tiny)) }, { @@ -180,16 +169,6 @@ BOOST_AUTO_TEST_CASE(success) 0xe2, 0x07, 0x0a, 0x05, 0x71, 0x99, 0x6d, 0xe2, 0x93, 0x4d, 0xf5, 0x3d }, - make_fv_vector( - std::int64_t(-3), - make_svoff_fv(5, 3, false), - nullptr, - 3.14f, - make_svoff_fv(13, 2, false), - nullptr, - date(2018u, 10u, 5u), - 3.10e-10 - ), make_fv_vector( std::int64_t(-3), "abc", @@ -220,14 +199,11 @@ BOOST_AUTO_TEST_CASE(success) { const auto& buffer = tc.from; deserialization_context ctx(buffer.data(), buffer.data() + buffer.size(), capabilities()); - std::vector actual; + std::vector actual(tc.meta.size()); error_code err; - deserialize_row(tc.encoding, ctx, tc.meta, tc.from.data(), actual, err); + deserialize_row(tc.encoding, ctx, tc.meta, actual.data(), err); BOOST_TEST(err == error_code()); - BOOST_TEST(actual == tc.expected_with_offsets); - - offsets_to_string_views(actual, tc.from.data()); BOOST_TEST(actual == tc.expected); } } @@ -361,10 +337,10 @@ BOOST_AUTO_TEST_CASE(error) { const auto& buffer = tc.from; deserialization_context ctx(buffer.data(), buffer.data() + buffer.size(), capabilities()); - std::vector actual; + std::vector actual(tc.meta.size()); error_code err; - deserialize_row(tc.encoding, ctx, tc.meta, buffer.data(), actual, err); + deserialize_row(tc.encoding, ctx, tc.meta, actual.data(), err); BOOST_TEST(err == error_code(tc.expected)); } } @@ -374,149 +350,86 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(with_execution_state) -BOOST_AUTO_TEST_CASE(text_rows) +BOOST_AUTO_TEST_CASE(text_row) { - std::vector row1{0x03, 0x76, 0x61, 0x6c, 0x02, 0x32, 0x31, 0x03, 0x30, 0x2e, 0x30}; - std::vector row2{0x03, 0x61, 0x62, 0x63, 0x02, 0x32, 0x30, 0x03, 0x30, 0x2e, 0x30}; - auto buff = concat_copy(row1, row2); - auto st = create_execution_state( - resultset_encoding::text, - {protocol_field_type::var_string, protocol_field_type::long_, protocol_field_type::float_} - ); - auto expected_fields = make_fv_vector(make_svoff_fv(1, 3, false), std::int64_t(21), 0.0f); - std::vector fields; + std::vector rowbuff{0x03, 0x76, 0x61, 0x6c, 0x02, 0x32, 0x31, 0x03, 0x30, 0x2e, 0x30}; + std::vector fields = make_fv_vector(42, "abc"); // from previous call + auto st = exec_builder(false) + .reset(resultset_encoding::text, &fields) + .meta({ + protocol_field_type::var_string, + protocol_field_type::long_, + protocol_field_type::float_, + }) + .build(); error_code err; diagnostics diag; - // First row deserialize_row( - boost::asio::const_buffer(buff.data(), row1.size()), + boost::asio::const_buffer(rowbuff.data(), rowbuff.size()), capabilities(), db_flavor::mysql, - buff.data(), st, - fields, err, diag ); BOOST_TEST(err == error_code()); BOOST_TEST(diag.server_message() == ""); - BOOST_TEST(!st.complete()); - BOOST_TEST(fields == expected_fields); - - // Second row (fields get appended to existing ones) - deserialize_row( - boost::asio::const_buffer(buff.data() + row1.size(), row2.size()), - capabilities(), - db_flavor::mysql, - buff.data(), - st, - fields, - err, - diag - ); - expected_fields.emplace_back(make_svoff_fv(12, 3, false)); - expected_fields.emplace_back(20); - expected_fields.emplace_back(0.0f); - - BOOST_TEST(err == error_code()); - BOOST_TEST(diag.server_message() == ""); - BOOST_TEST(!st.complete()); - BOOST_TEST(fields == expected_fields); - - // Convert offsets to string views - offsets_to_string_views(fields, buff.data()); - BOOST_TEST(fields == make_fv_vector("val", 21, 0.0f, "abc", 20, 0.0f)); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(fields == make_fv_vector(42, "abc", "val", 21, 0.0f)); } -BOOST_AUTO_TEST_CASE(binary_rows) +BOOST_AUTO_TEST_CASE(binary_row) { - std::vector row1{0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}; - std::vector row2{0x00, 0x08, 0x03, 0x6d, 0x61, 0x78}; - auto buff = concat_copy(row1, row2); - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_} - ); - auto expected_fields = make_fv_vector(make_svoff_fv(3, 3, false), std::int64_t(1901)); - std::vector fields; + std::vector rowbuff{0x00, 0x00, 0x03, 0x6d, 0x69, 0x6e, 0x6d, 0x07}; + std::vector fields = make_fv_vector(42, "abc"); // from previous call + auto st = exec_builder(false) + .reset(resultset_encoding::binary, &fields) + .meta({protocol_field_type::var_string, protocol_field_type::short_}) + .build(); error_code err; diagnostics diag; - // First row deserialize_row( - boost::asio::const_buffer(buff.data(), row1.size()), + boost::asio::const_buffer(rowbuff.data(), rowbuff.size()), capabilities(), db_flavor::mysql, - buff.data(), st, - fields, err, diag ); BOOST_TEST(err == error_code()); BOOST_TEST(diag.server_message() == ""); - BOOST_TEST(!st.complete()); - BOOST_TEST(fields == expected_fields); - - // Second row (fields get appended to existing ones) - deserialize_row( - boost::asio::const_buffer(buff.data() + row1.size(), row2.size()), - capabilities(), - db_flavor::mysql, - buff.data(), - st, - fields, - err, - diag - ); - expected_fields.emplace_back(make_svoff_fv(11, 3, false)); - expected_fields.emplace_back(nullptr); - - BOOST_TEST(err == error_code()); - BOOST_TEST(diag.server_message() == ""); - BOOST_TEST(!st.complete()); - BOOST_TEST(fields == expected_fields); - - // Convert offsets to string views - offsets_to_string_views(fields, buff.data()); - BOOST_TEST(fields == make_fv_vector("min", 1901, "max", nullptr)); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(fields == make_fv_vector(42, "abc", "min", 1901)); } BOOST_AUTO_TEST_CASE(ok_packet) { std::vector buff{0xfe, 0x01, 0x06, 0x02, 0x00, 0x09, 0x00, 0x02, 0x61, 0x62}; - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_} - ); - auto fields_before = make_fv_vector("abc", 20); // previous row - auto fields = fields_before; + std::vector fields = make_fv_vector("abc", 20); + auto st = exec_builder(false) + .reset(resultset_encoding::binary, &fields) + .meta({ + protocol_field_type::var_string, + protocol_field_type::short_, + }) + .build(); error_code err; diagnostics diag; - // First row - deserialize_row( - boost::asio::buffer(buff), - capabilities(), - db_flavor::mysql, - buff.data(), - st, - fields, - err, - diag - ); + deserialize_row(boost::asio::buffer(buff), capabilities(), db_flavor::mysql, st, err, diag); BOOST_TEST(err == error_code()); BOOST_TEST(diag.server_message() == ""); BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 1u); - BOOST_TEST(st.last_insert_id() == 6u); - BOOST_TEST(st.warning_count() == 9u); - BOOST_TEST(st.info() == "ab"); - BOOST_TEST(fields == fields_before); // they didn't change + BOOST_TEST(st.get_affected_rows(0) == 1u); + BOOST_TEST(st.get_last_insert_id(0) == 6u); + BOOST_TEST(st.get_warning_count(0) == 9u); + BOOST_TEST(st.get_info(0) == "ab"); + BOOST_TEST(fields == make_fv_vector("abc", 20)); // they didn't change } BOOST_AUTO_TEST_CASE(error) @@ -570,25 +483,19 @@ BOOST_AUTO_TEST_CASE(error) { BOOST_TEST_CONTEXT(tc.name) { - auto st = create_execution_state( - resultset_encoding::binary, - {protocol_field_type::var_string, protocol_field_type::short_} - ); std::vector fields; + auto st = exec_builder(false) + .reset(resultset_encoding::binary, &fields) + .meta({ + protocol_field_type::var_string, + protocol_field_type::short_, + }) + .build(); error_code err; diagnostics diag; // First row - deserialize_row( - boost::asio::buffer(tc.buffer), - capabilities(), - db_flavor::mysql, - tc.buffer.data(), - st, - fields, - err, - diag - ); + deserialize_row(boost::asio::buffer(tc.buffer), capabilities(), db_flavor::mysql, st, err, diag); BOOST_TEST(err == tc.expected_error); BOOST_TEST(diag.server_message() == tc.expected_info); diff --git a/test/unit/detail/protocol/deserialize_text_field.cpp b/test/unit/detail/protocol/deserialize_text_field.cpp index ad13ca9c..7cd1677d 100644 --- a/test/unit/detail/protocol/deserialize_text_field.cpp +++ b/test/unit/detail/protocol/deserialize_text_field.cpp @@ -23,7 +23,7 @@ #include #include -#include "create_meta.hpp" +#include "creation/create_meta.hpp" #include "printing.hpp" #include "test_common.hpp" @@ -433,27 +433,20 @@ std::vector make_all_samples() BOOST_DATA_TEST_CASE(ok, data::make(make_all_samples())) { - auto meta = create_meta(sample.type, sample.flags, static_cast(sample.decimals), sample.collation); - const std::uint8_t* buffer_first = reinterpret_cast(sample.from.data()); - field_view actual_value; + auto meta = meta_builder(sample.type) + .flags(sample.flags) + .decimals(static_cast(sample.decimals)) + .collation(sample.collation) + .build(); + field_view actual_value; auto err = deserialize_text_field( sample.from, meta, - buffer_first, actual_value ); + BOOST_TEST(err == deserialize_errc::ok); - - // Strings are representd as string view offsets - if (sample.expected.is_string() || sample.expected.is_blob()) - { - std::size_t expected_offset = sample.expected.is_string() ? sample.expected.get_string().size() : sample.expected.get_blob().size(); - field_view expected_offset_fv = make_svoff_fv(0, expected_offset, sample.expected.is_blob()); - BOOST_TEST(actual_value == expected_offset_fv); - field_view_access::offset_to_string_view(actual_value, buffer_first); - } - BOOST_TEST(actual_value == sample.expected); } @@ -700,10 +693,11 @@ std::vector make_all_samples() BOOST_DATA_TEST_CASE(error, data::make(make_all_samples())) { - auto meta = create_meta(sample.type, sample.flags, static_cast(sample.decimals)); - auto buffer_first = reinterpret_cast(sample.from.data()); + auto meta = meta_builder(sample.type).flags(sample.flags).decimals(static_cast(sample.decimals)).build(); + field_view actual_value; - auto err = deserialize_text_field(sample.from, meta, buffer_first, actual_value); + auto err = deserialize_text_field(sample.from, meta, actual_value); + BOOST_TEST(err == sample.expected_err); } diff --git a/test/unit/detail/protocol/execution_state_impl.cpp b/test/unit/detail/protocol/execution_state_impl.cpp new file mode 100644 index 00000000..11cfe0fc --- /dev/null +++ b/test/unit/detail/protocol/execution_state_impl.cpp @@ -0,0 +1,939 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "creation/create_execution_state.hpp" +#include "creation/create_message_struct.hpp" +#include "printing.hpp" +#include "test_common.hpp" + +using boost::mysql::column_type; +using boost::mysql::field_view; +using boost::mysql::metadata_collection_view; +using boost::mysql::metadata_mode; +using boost::mysql::row_view; +using boost::mysql::rows; +using boost::mysql::rows_view; +using boost::mysql::string_view; +using boost::mysql::detail::execution_state_impl; +using boost::mysql::detail::protocol_field_type; +using boost::mysql::detail::resultset_container; +using boost::mysql::detail::resultset_encoding; + +using namespace boost::mysql::test; + +namespace { + +// State checking +void check_should_read_head(const execution_state_impl& st) +{ + BOOST_TEST(st.should_read_head()); + BOOST_TEST(!st.should_read_meta()); + BOOST_TEST(!st.should_read_rows()); + BOOST_TEST(!st.complete()); +} + +void check_should_read_meta(const execution_state_impl& st) +{ + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(st.should_read_meta()); + BOOST_TEST(!st.should_read_rows()); + BOOST_TEST(!st.complete()); +} + +void check_should_read_rows(const execution_state_impl& st) +{ + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(!st.should_read_meta()); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(!st.complete()); +} + +void check_complete(const execution_state_impl& st) +{ + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(!st.should_read_meta()); + BOOST_TEST(!st.should_read_rows()); + BOOST_TEST(st.complete()); +} + +// Metadata +std::vector create_meta_r1() +{ + return {protocol_field_type::tiny, protocol_field_type::var_string}; +} + +std::vector create_meta_r2() { return {protocol_field_type::bit}; } + +void check_meta_r1(metadata_collection_view meta) +{ + BOOST_TEST_REQUIRE(meta.size() == 2u); + BOOST_TEST(meta[0].type() == column_type::tinyint); + BOOST_TEST(meta[1].type() == column_type::varchar); +} + +void check_meta_r2(metadata_collection_view meta) +{ + BOOST_TEST_REQUIRE(meta.size() == 1u); + BOOST_TEST(meta[0].type() == column_type::bit); +} + +void check_meta_r3(metadata_collection_view meta) +{ + BOOST_TEST_REQUIRE(meta.size() == 3u); + BOOST_TEST(meta[0].type() == column_type::float_); + BOOST_TEST(meta[1].type() == column_type::double_); + BOOST_TEST(meta[2].type() == column_type::tinyint); +} + +void check_meta_empty(metadata_collection_view meta) { BOOST_TEST(meta.size() == 0u); } + +// OK packet data checking +boost::mysql::detail::ok_packet create_ok_r1(bool more_results = false) +{ + return ok_builder() + .affected_rows(1) + .last_insert_id(2) + .warnings(4) + .info("Information") + .more_results(more_results) + .build(); +} + +boost::mysql::detail::ok_packet create_ok_r2(bool more_results = false) +{ + return ok_builder() + .affected_rows(5) + .last_insert_id(6) + .warnings(8) + .info("more_info") + .more_results(more_results) + .out_params(true) + .build(); +} + +boost::mysql::detail::ok_packet create_ok_r3() +{ + return ok_builder().affected_rows(10).last_insert_id(11).warnings(12).info("").build(); +} + +void check_ok_r1(const execution_state_impl& st, std::size_t idx) +{ + BOOST_TEST(st.get_affected_rows(idx) == 1u); + BOOST_TEST(st.get_last_insert_id(idx) == 2u); + BOOST_TEST(st.get_warning_count(idx) == 4u); + BOOST_TEST(st.get_info(idx) == "Information"); + BOOST_TEST(st.get_is_out_params(idx) == false); +} + +void check_ok_r2(const execution_state_impl& st, std::size_t idx) +{ + BOOST_TEST(st.get_affected_rows(idx) == 5u); + BOOST_TEST(st.get_last_insert_id(idx) == 6u); + BOOST_TEST(st.get_warning_count(idx) == 8u); + BOOST_TEST(st.get_info(idx) == "more_info"); + BOOST_TEST(st.get_is_out_params(idx) == true); +} + +void check_ok_r3(const execution_state_impl& st, std::size_t idx) +{ + BOOST_TEST(st.get_affected_rows(idx) == 10u); + BOOST_TEST(st.get_last_insert_id(idx) == 11u); + BOOST_TEST(st.get_warning_count(idx) == 12u); + BOOST_TEST(st.get_info(idx) == ""); + BOOST_TEST(st.get_is_out_params(idx) == false); +} + +// Rows. Note that this doesn't handle copying strings into the internal rows - +// this is not the responsibility of this component +template +void add_row(execution_state_impl& st, const Args&... args) +{ + assert(sizeof...(Args) == st.current_resultset_meta().size()); + auto fields = make_fv_arr(args...); + auto* storage = st.add_row(); + for (std::size_t i = 0; i < fields.size(); ++i) + { + storage[i] = fields[i]; + } +} + +BOOST_AUTO_TEST_SUITE(test_execution_state_impl) + +BOOST_AUTO_TEST_SUITE(test_resultset_container) +BOOST_AUTO_TEST_CASE(append_from_empty) +{ + // Initial + resultset_container c; + BOOST_TEST(c.empty()); + BOOST_TEST(c.size() == 0u); + + // Append first + c.emplace_back().num_rows = 1; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 1u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c.back().num_rows == 1u); + + // Append second + c.emplace_back().num_rows = 2; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 2u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c[1].num_rows == 2u); + BOOST_TEST(c.back().num_rows == 2u); + + // Append third + c.emplace_back().num_rows = 3; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 3u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c[1].num_rows == 2u); + BOOST_TEST(c[2].num_rows == 3u); + BOOST_TEST(c.back().num_rows == 3u); +} + +BOOST_AUTO_TEST_CASE(append_from_cleared) +{ + // Initial + resultset_container c; + c.emplace_back().num_rows = 42u; + c.emplace_back().num_rows = 43u; + + // Clear + c.clear(); + BOOST_TEST(c.empty()); + BOOST_TEST(c.size() == 0u); + + // Append first + c.emplace_back().num_rows = 1; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 1u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c.back().num_rows == 1u); + + // Append second + c.emplace_back().num_rows = 2; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 2u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c[1].num_rows == 2u); + BOOST_TEST(c.back().num_rows == 2u); + + // Append third + c.emplace_back().num_rows = 3; + BOOST_TEST(!c.empty()); + BOOST_TEST(c.size() == 3u); + BOOST_TEST(c[0].num_rows == 1u); + BOOST_TEST(c[1].num_rows == 2u); + BOOST_TEST(c[2].num_rows == 3u); + BOOST_TEST(c.back().num_rows == 3u); +} + +BOOST_AUTO_TEST_CASE(clear_empty) +{ + // Initial + resultset_container c; + c.clear(); + BOOST_TEST(c.empty()); + BOOST_TEST(c.size() == 0u); +} + +BOOST_AUTO_TEST_CASE(several_clears) +{ + // Initial + resultset_container c; + c.emplace_back().num_rows = 42u; + + // Clear + c.clear(); + BOOST_TEST(c.empty()); + BOOST_TEST(c.size() == 0u); + + // Append again + c.emplace_back().num_rows = 1; + c.emplace_back().num_rows = 2; + + // Clear again + c.clear(); + BOOST_TEST(c.empty()); + BOOST_TEST(c.size() == 0u); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(append_false) + +struct append_false_fixture +{ + std::vector fields; + execution_state_impl st{false}; + + append_false_fixture() { st.reset(resultset_encoding::text, &fields); } +}; + +BOOST_FIXTURE_TEST_CASE(one_resultset_data, append_false_fixture) +{ + // Initial. Verify that we clear any previous result + st = exec_builder(false) + .reset(&fields) + .meta({protocol_field_type::geometry}) + .rows(makerows(1, makebv("\0\0"), makebv(""))) + .build(); + st.reset(resultset_encoding::text, &fields); + check_should_read_head(st); + + // Head indicates resultset with metadata + st.on_num_meta(2); + check_should_read_meta(st); + + // First metadata + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + check_should_read_meta(st); + + // Second metadata, ready to read rows + st.on_meta(create_coldef(protocol_field_type::var_string), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r1(st.current_resultset_meta()); + check_meta_r1(st.get_meta(0)); + + // Rows + st.on_row_batch_start(); + add_row(st, 10, "abc"); + add_row(st, 20, "cdef"); + + // End of resultset + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r1(st.get_meta(0)); + BOOST_TEST(st.get_external_rows() == makerows(2, 10, "abc", 20, "cdef")); + check_ok_r1(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(one_resultset_empty, append_false_fixture) +{ + // Directly end of resultet, no meta + st.on_head_ok_packet(create_ok_r1()); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_ok_r1(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(two_resultsets_data_data, append_false_fixture) +{ + // Resultset r1 + st = exec_builder(false) + .reset(&fields) + .meta(create_meta_r1()) + .rows(makerows(2, 10, "abc", 20, "def")) + .build(); + + // OK packet indicates more results + st.on_row_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + check_meta_r1(st.get_meta(0)); + check_ok_r1(st, 0); + + // Resultset r2: indicates resultset with meta + st.on_num_meta(1); + check_should_read_meta(st); + + // First packet + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r2(st.current_resultset_meta()); + check_meta_r2(st.get_meta(0)); + + // Rows + st.on_row_batch_start(); + add_row(st, 90u); + + // OK packet, no more resultsets + st.on_row_ok_packet(create_ok_r2()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r2(st.get_meta(0)); + BOOST_TEST(st.get_external_rows() == makerows(1, 90u)); + check_ok_r2(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(two_resultsets_empty_data, append_false_fixture) +{ + // Resultset r1 + st.on_head_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + check_meta_empty(st.get_meta(0)); + check_ok_r1(st, 0); + + // Resultset r2: indicates data + st.on_num_meta(1); + check_should_read_meta(st); + + // Metadata packet + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r2(st.current_resultset_meta()); + check_meta_r2(st.get_meta(0)); + + // Rows + st.on_row_batch_start(); + add_row(st, 90u); + + // Final OK packet + st.on_row_ok_packet(create_ok_r2()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r2(st.get_meta(0)); + BOOST_TEST(st.get_external_rows() == makerows(1, 90u)); + check_ok_r2(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(two_resultsets_data_empty, append_false_fixture) +{ + // Resultset r1 + st = exec_builder(false).reset(&fields).meta(create_meta_r1()).build(); + + // OK packet indicates more results + st.on_row_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + check_meta_r1(st.get_meta(0)); + check_ok_r1(st, 0); + + // OK packet for 2nd result + st.on_head_ok_packet(create_ok_r2()); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_ok_r2(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(two_resultsets_empty_empty, append_false_fixture) +{ + // OK packet indicates more results + st.on_head_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + check_meta_empty(st.get_meta(0)); + check_ok_r1(st, 0); + + // OK packet for 2nd result + st.on_head_ok_packet(create_ok_r2()); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_ok_r2(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(three_resultsets_empty_empty_data, append_false_fixture) +{ + // Two first resultsets + st.on_head_ok_packet(create_ok_r1(true)); + st.on_head_ok_packet(create_ok_r2(true)); + check_should_read_head(st); + check_meta_empty(st.get_meta(0)); + check_ok_r2(st, 0); + + // Resultset r3: head indicates resultset with metadata + st.on_num_meta(3); + check_should_read_meta(st); + + // Metadata + st.on_meta(create_coldef(protocol_field_type::float_), metadata_mode::minimal); + check_should_read_meta(st); + + st.on_meta(create_coldef(protocol_field_type::double_), metadata_mode::minimal); + check_should_read_meta(st); + + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r3(st.current_resultset_meta()); + check_meta_r3(st.get_meta(0)); + + // Rows + st.on_row_batch_start(); + add_row(st, 4.2f, 90.0, 9); + + // End of resultset + st.on_row_ok_packet(create_ok_r3()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r3(st.get_meta(0)); + BOOST_TEST(st.get_external_rows() == makerows(3, 4.2f, 90.0, 9)); + check_ok_r3(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(three_resultsets_data_empty_data, append_false_fixture) +{ + // Two first resultsets + st = exec_builder(false) + .reset(&fields) + .meta(create_meta_r1()) + .rows(makerows(2, 40, "abc", 50, "def")) + .ok(create_ok_r1(true)) + .build(); + st.on_head_ok_packet(create_ok_r2(true)); + check_should_read_head(st); + check_meta_empty(st.get_meta(0)); + check_ok_r2(st, 0); + + // Resultset r3: head indicates resultset with metadata + st.on_num_meta(3); + check_should_read_meta(st); + + // Metadata + st.on_meta(create_coldef(protocol_field_type::float_), metadata_mode::minimal); + st.on_meta(create_coldef(protocol_field_type::double_), metadata_mode::minimal); + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r3(st.current_resultset_meta()); + check_meta_r3(st.get_meta(0)); + + // Rows + st.on_row_batch_start(); + add_row(st, 4.2f, 90.0, 9); + + // End of resultset + st.on_row_ok_packet(create_ok_r3()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r3(st.get_meta(0)); + BOOST_TEST(st.get_external_rows() == makerows(3, 4.2f, 90.0, 9)); + check_ok_r3(st, 0); +} + +BOOST_FIXTURE_TEST_CASE(info_string_ownserhip, append_false_fixture) +{ + // OK packet received, doesn't own the string + std::string info = "Some info"; + st.on_head_ok_packet(ok_builder().more_results(true).info(info).build()); + + // st does, so changing info doesn't affect + info = "other info"; + BOOST_TEST(st.get_info(0) == "Some info"); + + // Repeat the process for row OK packet + st.on_num_meta(1); + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::full); + st.on_row_ok_packet(ok_builder().info(info).build()); + info = "abcdfefgh"; + BOOST_TEST(st.get_info(0) == "other info"); +} + +BOOST_FIXTURE_TEST_CASE(multiple_row_batches, append_false_fixture) +{ + // Head and meta + st = exec_builder(false) + .reset(&fields) + .meta({protocol_field_type::tiny, protocol_field_type::var_string}) + .build(); + + // Row batch 1 + st.on_row_batch_start(); + add_row(st, 10, "abc"); + add_row(st, 20, "cdef"); + st.on_row_batch_finish(); + check_should_read_rows(st); + BOOST_TEST(st.get_external_rows() == makerows(2, 10, "abc", 20, "cdef")); + + // Row batch 2 + st.on_row_batch_start(); + add_row(st, 40, nullptr); + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); + check_complete(st); + BOOST_TEST(st.get_external_rows() == makerows(2, 40, nullptr)); + BOOST_TEST(fields.size() == 2u); // space was re-used +} + +BOOST_FIXTURE_TEST_CASE(empty_row_batch, append_false_fixture) +{ + // Head and meta + st = exec_builder(false) + .reset(&fields) + .meta({protocol_field_type::tiny, protocol_field_type::var_string}) + .build(); + + // Row batch 1 + st.on_row_batch_start(); + add_row(st, 10, "abc"); + add_row(st, 20, "cdef"); + st.on_row_batch_finish(); + BOOST_TEST(st.get_external_rows() == makerows(2, 10, "abc", 20, "cdef")); + + // Row batch 2 + st.on_row_batch_start(); + add_row(st, 40, nullptr); + st.on_row_batch_finish(); + BOOST_TEST(st.get_external_rows() == makerows(2, 40, nullptr)); + BOOST_TEST(fields.size() == 2u); // space was re-used + + // End of resultset + st.on_row_batch_start(); + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); + BOOST_TEST(st.get_external_rows() == makerows(2)); + check_complete(st); +} +BOOST_AUTO_TEST_SUITE_END() // append_false + +// +// append true +// +BOOST_AUTO_TEST_SUITE(append_true) + +BOOST_AUTO_TEST_CASE(one_resultset_data) +{ + // Initial. Check that we reset any previous state + auto st = exec_builder(true) + .reset(resultset_encoding::binary) + .meta({protocol_field_type::geometry}) + .rows(makerows(1, makebv("\0\0"), makebv("abc"))) + .ok(ok_builder().affected_rows(40).info("some_info").more_results(true).build()) + .meta({protocol_field_type::var_string}) + .rows(makerows(1, "aaaa", "bbbb")) + .ok(ok_builder().info("more_info").more_results(true).build()) + .build(); + st.reset(resultset_encoding::binary, nullptr); + check_should_read_head(st); + + // Head indicates resultset with two columns + st.on_num_meta(2); + check_should_read_meta(st); + + // First meta + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + check_should_read_meta(st); + + // Second meta, ready to read rows + st.on_meta(create_coldef(protocol_field_type::var_string), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r1(st.current_resultset_meta()); + + // Rows + st.on_row_batch_start(); + add_row(st, 42, "abc"); + + // End of resultset + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); // EOF is part of the batch + check_complete(st); + check_meta_r1(st.get_meta(0)); + check_ok_r1(st, 0); + BOOST_TEST(st.num_resultsets() == 1u); + BOOST_TEST(st.get_rows(0) == makerows(2, 42, "abc")); + BOOST_TEST(st.get_out_params() == row_view()); +} + +BOOST_AUTO_TEST_CASE(one_resultset_empty) +{ + // Initial + execution_state_impl st(true); + st.reset(resultset_encoding::text, nullptr); + check_should_read_head(st); + + // End of resultset + st.on_head_ok_packet(create_ok_r1()); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_ok_r1(st, 0); + BOOST_TEST(st.num_resultsets() == 1u); + BOOST_TEST(st.get_rows(0) == rows_view()); + BOOST_TEST(st.get_out_params() == row_view()); +} + +BOOST_AUTO_TEST_CASE(two_resultsets_data_data) +{ + // Resultset r1 + auto st = exec_builder(true).meta(create_meta_r1()).rows(makerows(2, 42, "abc", 50, "def")).build(); + + // OK packet indicates more results + st.on_row_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + + // Resultset r2: indicates resultset with meta + st.on_num_meta(1); + check_should_read_meta(st); + + // Meta + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r2(st.current_resultset_meta()); + + // Row + st.on_row_batch_start(); + add_row(st, 70); + check_should_read_rows(st); + + // OK packet, no more resultsets + st.on_row_ok_packet(create_ok_r2()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_r1(st.get_meta(0)); + check_meta_r2(st.get_meta(1)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + BOOST_TEST(st.num_resultsets() == 2u); + BOOST_TEST(st.get_rows(0) == makerows(2, 42, "abc", 50, "def")); + BOOST_TEST(st.get_rows(1) == makerows(1, 70)); + BOOST_TEST(st.get_out_params() == makerow(70)); +} + +BOOST_AUTO_TEST_CASE(two_resultsets_empty_data) +{ + // Resultset r1: same as the single case + execution_state_impl st(true); + st.on_head_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + + // Resultset r2: indicates data + st.on_num_meta(1); + check_should_read_meta(st); + + // Metadata packet + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r2(st.current_resultset_meta()); + + // Rows + st.on_row_batch_start(); + add_row(st, 70); + check_should_read_rows(st); + + // Final OK packet + st.on_row_ok_packet(create_ok_r2()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_meta_r2(st.get_meta(1)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + BOOST_TEST(st.num_resultsets() == 2u); + BOOST_TEST(st.get_rows(0) == rows_view()); + BOOST_TEST(st.get_rows(1) == makerows(1, 70)); + BOOST_TEST(st.get_out_params() == makerow(70)); +} + +// Note: this tests also an edge case where a resultset indicates +// that contains OUT parameters but is empty +BOOST_AUTO_TEST_CASE(two_resultsets_data_empty) +{ + // Resultset r1: equivalent to single resultset case + auto st = exec_builder(true).meta(create_meta_r1()).rows(makerows(2, 42, "abc", 50, "def")).build(); + + // OK packet indicates more results + st.on_row_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + + // OK packet for 2nd result + st.on_head_ok_packet(create_ok_r2()); + check_complete(st); + check_meta_r1(st.get_meta(0)); + check_meta_empty(st.get_meta(1)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + BOOST_TEST(st.num_resultsets() == 2u); + BOOST_TEST(st.get_rows(0) == makerows(2, 42, "abc", 50, "def")); + BOOST_TEST(st.get_rows(1) == rows_view()); + BOOST_TEST(st.get_out_params() == row_view()); +} + +BOOST_AUTO_TEST_CASE(two_resultsets_empty_empty) +{ + // Resultset r1: equivalent to single resultset case + execution_state_impl st(true); + st.on_head_ok_packet(create_ok_r1(true)); + check_should_read_head(st); + + // OK packet for 2nd result + st.on_head_ok_packet(create_ok_r2()); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_meta_empty(st.get_meta(1)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + BOOST_TEST(st.num_resultsets() == 2u); + BOOST_TEST(st.get_rows(0) == rows_view()); + BOOST_TEST(st.get_rows(1) == rows_view()); + BOOST_TEST(st.get_out_params() == row_view()); +} + +BOOST_AUTO_TEST_CASE(three_resultsets_empty_empty_data) +{ + // Two first resultsets + auto st = exec_builder(true).ok(create_ok_r1(true)).build(); + st.on_head_ok_packet(create_ok_r2(true)); + check_should_read_head(st); + + // Resultset r3: head indicates resultset with metadata + st.on_num_meta(3); + check_should_read_meta(st); + + // Metadata + st.on_meta(create_coldef(protocol_field_type::float_), metadata_mode::minimal); + check_should_read_meta(st); + + st.on_meta(create_coldef(protocol_field_type::double_), metadata_mode::minimal); + check_should_read_meta(st); + + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + check_should_read_rows(st); + check_meta_r3(st.current_resultset_meta()); + + // Read rows + st.on_row_batch_start(); + add_row(st, 4.2f, 5.0, 8); + add_row(st, 42.0f, 50.0, 80); + + // End of resultset + st.on_row_ok_packet(create_ok_r3()); + st.on_row_batch_finish(); + check_complete(st); + check_meta_empty(st.get_meta(0)); + check_meta_empty(st.get_meta(1)); + check_meta_r3(st.get_meta(2)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + check_ok_r3(st, 2); + BOOST_TEST(st.num_resultsets() == 3u); + BOOST_TEST(st.get_rows(0) == rows_view()); + BOOST_TEST(st.get_rows(1) == rows_view()); + BOOST_TEST(st.get_rows(2) == makerows(3, 4.2f, 5.0, 8, 42.0f, 50.0, 80)); + BOOST_TEST(st.get_out_params() == row_view()); +} + +// Verify that we do row slicing correctly +BOOST_AUTO_TEST_CASE(three_resultsets_data_data_data) +{ + // Two first resultets + auto st = exec_builder(true) + .meta(create_meta_r1()) + .rows(makerows(2, 42, "abc", 50, "def")) + .ok(create_ok_r1(true)) + .meta(create_meta_r2()) + .rows(makerows(1, 60)) + .build(); + + // OK packet indicates more results + st.on_row_ok_packet(create_ok_r2(true)); + + // Third resultset + st.on_num_meta(3); + st.on_meta(create_coldef(protocol_field_type::float_), metadata_mode::minimal); + st.on_meta(create_coldef(protocol_field_type::double_), metadata_mode::minimal); + st.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::minimal); + st.on_row_batch_start(); + add_row(st, 4.2f, 5.0, 8); + add_row(st, 42.0f, 50.0, 80); + st.on_row_ok_packet(create_ok_r3()); + st.on_row_batch_finish(); + + // Check results + check_complete(st); + check_meta_r1(st.get_meta(0)); + check_meta_r2(st.get_meta(1)); + check_meta_r3(st.get_meta(2)); + check_ok_r1(st, 0); + check_ok_r2(st, 1); + check_ok_r3(st, 2); + BOOST_TEST(st.num_resultsets() == 3u); + BOOST_TEST(st.get_rows(0) == makerows(2, 42, "abc", 50, "def")); + BOOST_TEST(st.get_rows(1) == makerows(1, 60)); + BOOST_TEST(st.get_rows(2) == makerows(3, 4.2f, 5.0, 8, 42.0f, 50.0, 80)); + BOOST_TEST(st.get_out_params() == makerow(60)); +} + +BOOST_AUTO_TEST_CASE(info_string_ownserhip) +{ + execution_state_impl st(true); + + // Head OK packet + std::string info = "Some info"; + st.on_head_ok_packet(ok_builder().more_results(true).info(info).build()); + + // Empty OK packet + info = ""; + st.on_head_ok_packet(ok_builder().more_results(true).info(info).build()); + + // Row OK packet + info = "other info"; + st.on_num_meta(1); + st.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::full); + st.on_row_ok_packet(ok_builder().info(info).build()); + info = "abcdfefgh"; + BOOST_TEST(st.get_info(0) == "Some info"); + BOOST_TEST(st.get_info(1) == ""); + BOOST_TEST(st.get_info(2) == "other info"); +} + +BOOST_AUTO_TEST_CASE(multiple_row_batches) +{ + // Initial + auto st = exec_builder(true).meta({protocol_field_type::tiny, protocol_field_type::var_string}).build(); + + // First batch + st.on_row_batch_start(); + add_row(st, 42, "abc"); + add_row(st, 50, "bdef"); + st.on_row_batch_finish(); + + // Second batch (only one row) + st.on_row_batch_start(); + add_row(st, 60, "pov"); + + // End of resultset + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); + check_complete(st); + BOOST_TEST(st.num_resultsets() == 1u); + BOOST_TEST(st.get_rows(0) == makerows(2, 42, "abc", 50, "bdef", 60, "pov")); +} + +BOOST_AUTO_TEST_CASE(empty_row_batch) +{ + // Initial + auto st = exec_builder(true).meta({protocol_field_type::tiny, protocol_field_type::var_string}).build(); + + // No rows, directly eof + st.on_row_batch_start(); + st.on_row_ok_packet(create_ok_r1()); + st.on_row_batch_finish(); + check_complete(st); + BOOST_TEST(st.num_resultsets() == 1u); + BOOST_TEST(st.get_rows(0) == makerows(2)); // empty but with 2 cols +} +BOOST_AUTO_TEST_SUITE_END() // append true + +BOOST_AUTO_TEST_CASE(reset) +{ + auto st = exec_builder(true) + .reset(resultset_encoding::binary) + .seqnum(90u) + .meta({protocol_field_type::bit}) + .build(); + st.reset(resultset_encoding::text, nullptr); + BOOST_TEST(st.encoding() == resultset_encoding::text); + BOOST_TEST(st.sequence_number() == 0u); + BOOST_TEST(st.is_append_mode() == true); // doesn't get reset +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/detail/protocol/process_error_packet.cpp b/test/unit/detail/protocol/process_error_packet.cpp index 8c1f5f84..01e021e8 100644 --- a/test/unit/detail/protocol/process_error_packet.cpp +++ b/test/unit/detail/protocol/process_error_packet.cpp @@ -19,7 +19,7 @@ #include -#include "create_message.hpp" +#include "creation/create_message.hpp" using namespace boost::mysql::detail; using boost::mysql::client_errc; diff --git a/test/unit/diagnostics.cpp b/test/unit/diagnostics.cpp index 98bdecf0..a2f9dd3a 100644 --- a/test/unit/diagnostics.cpp +++ b/test/unit/diagnostics.cpp @@ -9,7 +9,7 @@ #include -#include "create_diagnostics.hpp" +#include "creation/create_diagnostics.hpp" #include "printing.hpp" using boost::mysql::diagnostics; diff --git a/test/unit/execution_state.cpp b/test/unit/execution_state.cpp index acae1cfc..af7c7aee 100644 --- a/test/unit/execution_state.cpp +++ b/test/unit/execution_state.cpp @@ -9,72 +9,125 @@ #include #include -#include -#include #include -#include #include #include -#include "create_execution_state.hpp" -#include "create_message.hpp" +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message_struct.hpp" +#include "test_common.hpp" using namespace boost::mysql::test; -using boost::mysql::column_type; -using boost::mysql::execution_state; -using boost::mysql::detail::column_definition_packet; -using boost::mysql::detail::execution_state_access; -using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::resultset_encoding; +using namespace boost::mysql; +using detail::protocol_field_type; namespace { - BOOST_AUTO_TEST_SUITE(test_execution_state) -BOOST_AUTO_TEST_CASE(member_fns) +// The functionality has been tested in execution_state_impl already. +// Just spotchecks here +BOOST_AUTO_TEST_CASE(spotchecks) { - // Construction + std::vector fields; execution_state st; + auto& impl = boost::mysql::detail::execution_state_access::get_impl(st); + + // Initial + BOOST_TEST(st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(!st.should_read_rows()); BOOST_TEST(!st.complete()); BOOST_TEST(st.meta().size() == 0u); // Reset - execution_state_access::reset(st, resultset_encoding::binary); + impl.reset(detail::resultset_encoding::text, &fields); + BOOST_TEST(st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(!st.should_read_rows()); BOOST_TEST(!st.complete()); - BOOST_TEST(st.meta().size() == 0u); - - // Add meta - column_definition_packet pack{}; - pack.type = protocol_field_type::var_string; - execution_state_access::add_meta(st, pack, boost::mysql::metadata_mode::minimal); - pack.type = protocol_field_type::bit; - execution_state_access::add_meta(st, pack, boost::mysql::metadata_mode::minimal); + // Reading meta + impl.on_num_meta(1); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(st.should_read_head()); + BOOST_TEST(!st.should_read_rows()); BOOST_TEST(!st.complete()); - BOOST_TEST(st.meta().size() == 2u); - BOOST_TEST(st.meta()[0].type() == column_type::varchar); - BOOST_TEST(st.meta()[1].type() == column_type::bit); - // Complete the op - execution_state_access::complete(st, create_ok_packet(4, 1, 0, 3, "info")); + // Reading rows + impl.on_meta(create_coldef(protocol_field_type::bit), metadata_mode::full); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(!st.complete()); + check_meta(st.meta(), {column_type::bit}); + + // Reading a row leaves it in the same state + impl.on_row_batch_start(); + *impl.add_row() = field_view(42u); + impl.on_row_batch_finish(); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(!st.complete()); + BOOST_TEST(fields == make_fv_vector(42u)); + + // End of first resultset + impl.on_row_batch_start(); + impl.on_row_ok_packet(ok_builder() + .affected_rows(1) + .last_insert_id(2) + .warnings(4) + .info("abc") + .more_results(true) + .out_params(true) + .build()); + impl.on_row_batch_finish(); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(st.should_read_head()); + BOOST_TEST(!st.should_read_rows()); + BOOST_TEST(!st.complete()); + check_meta(st.meta(), {column_type::bit}); + BOOST_TEST(st.affected_rows() == 1u); + BOOST_TEST(st.last_insert_id() == 2u); + BOOST_TEST(st.warning_count() == 4u); + BOOST_TEST(st.info() == "abc"); + BOOST_TEST(st.is_out_params()); + + // Second resultset meta + impl.on_num_meta(1); + impl.on_meta(create_coldef(protocol_field_type::tiny), metadata_mode::full); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(st.should_read_rows()); + BOOST_TEST(!st.complete()); + check_meta(st.meta(), {column_type::tinyint}); + + // Complete + impl.on_row_batch_start(); + impl.on_row_ok_packet(ok_builder().affected_rows(5).last_insert_id(6).warnings(7).info("bhu").build()); + impl.on_row_batch_finish(); + BOOST_TEST(!st.should_start_op()); + BOOST_TEST(!st.should_read_head()); + BOOST_TEST(!st.should_read_rows()); BOOST_TEST(st.complete()); - BOOST_TEST(st.affected_rows() == 4u); - BOOST_TEST(st.last_insert_id() == 1u); - BOOST_TEST(st.warning_count() == 3u); - BOOST_TEST(st.info() == "info"); - - // Reset - execution_state_access::reset(st, resultset_encoding::text); - BOOST_TEST(!st.complete()); - BOOST_TEST(st.meta().size() == 0u); + check_meta(st.meta(), {column_type::tinyint}); + BOOST_TEST(st.affected_rows() == 5u); + BOOST_TEST(st.last_insert_id() == 6u); + BOOST_TEST(st.warning_count() == 7u); + BOOST_TEST(st.info() == "bhu"); + BOOST_TEST(!st.is_out_params()); } +// Verify that the lifetime guarantees we make are correct BOOST_AUTO_TEST_CASE(move_constructor) { - // Construct an execution_state with value - auto st = create_execution_state(resultset_encoding::text, {protocol_field_type::var_string}); - execution_state_access::complete(st, create_ok_packet(2, 3, 0, 4, "small")); + // Construction + execution_state st = exec_builder(false) + .meta({protocol_field_type::var_string}) + .ok(ok_builder().info("small").build()) + .build_state(); // Obtain references auto meta = st.meta(); @@ -82,25 +135,25 @@ BOOST_AUTO_TEST_CASE(move_constructor) // Move construct execution_state st2(std::move(st)); - st = execution_state(); // Regression check - std::string impl SBO buffer + st = execution_state(); // Regression check // Make sure that views are still valid - BOOST_TEST_REQUIRE(meta.size() == 1u); - BOOST_TEST(meta[0].type() == column_type::varchar); + check_meta(meta, {column_type::varchar}); BOOST_TEST(info == "small"); // The new object holds the same data BOOST_TEST_REQUIRE(st2.complete()); - BOOST_TEST_REQUIRE(st2.meta().size() == 1u); - BOOST_TEST(st2.meta()[0].type() == column_type::varchar); + check_meta(st2.meta(), {column_type::varchar}); BOOST_TEST(st2.info() == "small"); } BOOST_AUTO_TEST_CASE(move_assignment) { // Construct an execution_state with value - auto st = create_execution_state(resultset_encoding::text, {protocol_field_type::var_string}); - execution_state_access::complete(st, create_ok_packet(2, 3, 0, 4, "small")); + execution_state st = exec_builder(false) + .meta({protocol_field_type::var_string}) + .ok(ok_builder().info("small").build()) + .build_state(); // Obtain references auto meta = st.meta(); @@ -112,14 +165,12 @@ BOOST_AUTO_TEST_CASE(move_assignment) st = execution_state(); // Regression check - std::string impl SBO buffer // Make sure that views are still valid - BOOST_TEST_REQUIRE(meta.size() == 1u); - BOOST_TEST(meta[0].type() == column_type::varchar); + check_meta(meta, {column_type::varchar}); BOOST_TEST(info == "small"); // The new object holds the same data BOOST_TEST_REQUIRE(st2.complete()); - BOOST_TEST_REQUIRE(st2.meta().size() == 1u); - BOOST_TEST(st2.meta()[0].type() == column_type::varchar); + check_meta(st2.meta(), {column_type::varchar}); BOOST_TEST(st2.info() == "small"); } diff --git a/test/unit/metadata.cpp b/test/unit/metadata.cpp index 4eff0fea..456e895d 100644 --- a/test/unit/metadata.cpp +++ b/test/unit/metadata.cpp @@ -8,13 +8,14 @@ #include #include -#include "create_meta.hpp" +#include + +#include "creation/create_message_struct.hpp" #include "printing.hpp" #include "test_common.hpp" using namespace boost::mysql::detail; using boost::mysql::column_type; -using boost::mysql::metadata; using namespace boost::mysql::test; BOOST_AUTO_TEST_SUITE(test_metadata) @@ -33,7 +34,7 @@ BOOST_AUTO_TEST_CASE(int_primary_key) protocol_field_type::long_, column_flags::pri_key | column_flags::auto_increment | column_flags::not_null, 0}; - auto meta = create_meta(msg, true); + auto meta = boost::mysql::detail::metadata_access::construct(msg, true); BOOST_TEST(meta.database() == "awesome"); BOOST_TEST(meta.table() == "test_table"); @@ -68,7 +69,7 @@ BOOST_AUTO_TEST_CASE(varchar_with_alias) protocol_field_type::var_string, 0, 0}; - auto meta = create_meta(msg, true); + auto meta = boost::mysql::detail::metadata_access::construct(msg, true); BOOST_TEST(meta.database() == "awesome"); BOOST_TEST(meta.table() == "child"); @@ -103,7 +104,7 @@ BOOST_AUTO_TEST_CASE(float_) protocol_field_type::float_, 0, 31}; - auto meta = create_meta(msg, true); + auto meta = boost::mysql::detail::metadata_access::construct(msg, true); BOOST_TEST(meta.database() == "awesome"); BOOST_TEST(meta.table() == "test_table"); @@ -138,7 +139,7 @@ BOOST_AUTO_TEST_CASE(dont_copy_strings) protocol_field_type::var_string, 0, 0}; - auto meta = create_meta(msg, false); + auto meta = boost::mysql::detail::metadata_access::construct(msg, false); BOOST_TEST(meta.database() == ""); BOOST_TEST(meta.table() == ""); @@ -159,4 +160,16 @@ BOOST_AUTO_TEST_CASE(dont_copy_strings) BOOST_TEST(!meta.is_set_to_now_on_update()); } +BOOST_AUTO_TEST_CASE(string_ownership) +{ + // Create the meta object + std::string colname = "col1"; + auto msg = create_coldef(protocol_field_type::float_, colname); + auto meta = boost::mysql::detail::metadata_access::construct(msg, true); + + // Check that we actually copy the data + colname = "abcd"; + BOOST_TEST(meta.column_name() == "col1"); +} + BOOST_AUTO_TEST_SUITE_END() // test_metadata diff --git a/test/unit/multifn.cpp b/test/unit/multifn.cpp new file mode 100644 index 00000000..6d44a299 --- /dev/null +++ b/test/unit/multifn.cpp @@ -0,0 +1,216 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +// Since integration tests can't reliably test multifunction operations +// that span over multiple messages, we test the complete multifn fllow in this unit tests. + +#include +#include +#include + +#include + +#include + +#include "assert_buffer_equals.hpp" +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message.hpp" +#include "creation/create_row_message.hpp" +#include "printing.hpp" +#include "test_common.hpp" +#include "test_connection.hpp" +#include "unit_netfun_maker.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using detail::protocol_field_type; + +namespace { + +using start_query_netm = netfun_maker_mem; +using read_resultset_head_netm = netfun_maker_mem; +using read_some_rows_netm = netfun_maker_mem; + +struct +{ + start_query_netm::signature start_query; + read_resultset_head_netm::signature read_resultset_head; + read_some_rows_netm::signature read_some_rows; + const char* name; +} all_fns[] = { + {start_query_netm::sync_errc(&test_connection::start_query), + read_resultset_head_netm::sync_errc(&test_connection::read_resultset_head), + read_some_rows_netm::sync_errc(&test_connection::read_some_rows), + "sync" }, + {start_query_netm::async_errinfo(&test_connection::async_start_query), + read_resultset_head_netm::async_errinfo(&test_connection::async_read_resultset_head), + read_some_rows_netm::async_errinfo(&test_connection::async_read_some_rows), + "async"}, +}; + +BOOST_AUTO_TEST_SUITE(test_multifn) + +BOOST_AUTO_TEST_CASE(separate_batches) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + test_connection conn; + conn.stream() + .add_message(create_message(1, {0x01})) + .add_message(create_coldef_message(2, protocol_field_type::var_string)) + .add_message(create_text_row_message(3, "abc")) + .add_message( + ok_msg_builder().seqnum(4).affected_rows(10u).info("1st").more_results(true).build_eof() + ) + .add_message(create_message(5, {0x01})) + .add_message(create_coldef_message(6, protocol_field_type::newdecimal)) + .add_message(concat_copy(create_text_row_message(7, "ab"), create_text_row_message(8, "plo"))) + .add_message(concat_copy( + create_text_row_message(9, "hju"), + ok_msg_builder().seqnum(10).affected_rows(30u).info("2nd").build_eof() + )); + + // Start + fns.start_query(conn, "SELECT 1", st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::varchar}); + + // 1st resultset, row + auto rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + BOOST_TEST(rv == makerows(1, "abc")); + + // 1st resultset, eof + rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(rv == makerows(1)); + BOOST_TEST(st.affected_rows() == 10u); + BOOST_TEST(st.info() == "1st"); + + // 2nd resultset, head + fns.read_resultset_head(conn, st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::decimal}); + + // 2nd resultset, row batch + rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + BOOST_TEST(rv == makerows(1, "ab", "plo")); + + // 2nd resultset, last row & eof + rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(rv == makerows(1, "hju")); + BOOST_TEST(st.affected_rows() == 30u); + BOOST_TEST(st.info() == "2nd"); + } + } +} + +// The server sent us a single, big message with everything +BOOST_AUTO_TEST_CASE(single_read) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + test_connection conn(buffer_params(4096)); + std::vector msgs; + concat(msgs, create_message(1, {0x01})); + concat(msgs, create_coldef_message(2, protocol_field_type::var_string)); + concat(msgs, create_text_row_message(3, "abc")); + concat( + msgs, + ok_msg_builder().seqnum(4).affected_rows(10u).info("1st").more_results(true).build_eof() + ); + concat(msgs, create_message(5, {0x01})); + concat(msgs, create_coldef_message(6, protocol_field_type::newdecimal)); + concat(msgs, create_text_row_message(7, "ab")); + concat(msgs, create_text_row_message(8, "plo")); + concat(msgs, create_text_row_message(9, "hju")); + concat(msgs, ok_msg_builder().seqnum(10).affected_rows(30u).info("2nd").build_eof()); + conn.stream().add_message(std::move(msgs)); + + // Start + fns.start_query(conn, "SELECT 1", st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::varchar}); + + // First resultset + auto rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(rv == makerows(1, "abc")); + BOOST_TEST(st.affected_rows() == 10u); + BOOST_TEST(st.info() == "1st"); + + // 2nd resultset, head + fns.read_resultset_head(conn, st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_rows()); + check_meta(st.meta(), {column_type::decimal}); + + // 2nd resultset + rv = fns.read_some_rows(conn, st).get(); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(rv == makerows(1, "ab", "plo", "hju")); + BOOST_TEST(st.affected_rows() == 30u); + BOOST_TEST(st.info() == "2nd"); + } + } +} + +BOOST_AUTO_TEST_CASE(empty_resultsets) +{ + for (auto fns : all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + execution_state st; + test_connection conn(buffer_params(4096)); + std::vector msgs; + concat( + msgs, + ok_msg_builder().seqnum(1).affected_rows(10u).info("1st").more_results(true).build_ok() + ); + concat( + msgs, + ok_msg_builder().seqnum(2).affected_rows(20u).info("2nd").more_results(true).build_ok() + ); + concat(msgs, ok_msg_builder().seqnum(3).affected_rows(30u).info("3rd").build_ok()); + conn.stream().add_message(std::move(msgs)); + + // Start + fns.start_query(conn, "SELECT 1", st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 10u); + BOOST_TEST(st.info() == "1st"); + + // 2nd resultset + fns.read_resultset_head(conn, st).validate_no_error(); + BOOST_TEST_REQUIRE(st.should_read_head()); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 20u); + BOOST_TEST(st.info() == "2nd"); + + // 3rd resultset + fns.read_resultset_head(conn, st).validate_no_error(); + BOOST_TEST_REQUIRE(st.complete()); + BOOST_TEST(st.meta().size() == 0u); + BOOST_TEST(st.affected_rows() == 30u); + BOOST_TEST(st.info() == "3rd"); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/regressions.cpp b/test/unit/regressions.cpp index 2a8ffe37..590978a7 100644 --- a/test/unit/regressions.cpp +++ b/test/unit/regressions.cpp @@ -18,7 +18,7 @@ #include "assert_buffer_equals.hpp" #include "buffer_concat.hpp" -#include "create_message.hpp" +#include "creation/create_message.hpp" #include "run_coroutine.hpp" #include "test_connection.hpp" @@ -41,8 +41,8 @@ BOOST_AUTO_TEST_CASE(side_effects_in_initiation) results result1, result2; // Resultsets will be complete as soon as a message is read - auto ok_packet_1 = create_ok_packet_message(1, 1, 6, 0, 9, "ab"); - auto ok_packet_2 = create_ok_packet_message(1, 2, 0, 0, 0, "uv"); + auto ok_packet_1 = ok_msg_builder().seqnum(1).affected_rows(1).build_ok(); + auto ok_packet_2 = ok_msg_builder().seqnum(1).affected_rows(2).build_ok(); conn.stream().add_message(ok_packet_2); conn.stream().add_message(ok_packet_1); diff --git a/test/unit/results.cpp b/test/unit/results.cpp index eb462b99..3a9dc6cd 100644 --- a/test/unit/results.cpp +++ b/test/unit/results.cpp @@ -8,65 +8,282 @@ #include #include -#include -#include -#include -#include +#include #include -#include "create_execution_state.hpp" -#include "create_message.hpp" +#include + +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message_struct.hpp" +#include "printing.hpp" #include "test_common.hpp" using namespace boost::mysql::test; using boost::mysql::column_type; using boost::mysql::results; -using boost::mysql::detail::execution_state_access; +using boost::mysql::rows; using boost::mysql::detail::protocol_field_type; -using boost::mysql::detail::results_access; -using boost::mysql::detail::resultset_encoding; namespace { BOOST_AUTO_TEST_SUITE(test_results) +results create_initial_results() +{ + return create_results({ + {{protocol_field_type::var_string}, + makerows(1, "abc", nullptr), + ok_builder().affected_rows(1).last_insert_id(2).warnings(3).info("1st").build()}, + + {{protocol_field_type::tiny}, + makerows(1, 42), + ok_builder().affected_rows(4).last_insert_id(5).warnings(6).info("2nd").out_params(true).build()}, + + {{}, rows(), ok_builder().info("3rd").build()} + }); +} + +struct fixture +{ + results result{create_initial_results()}; +}; + BOOST_AUTO_TEST_CASE(has_value) { // Default construction results result; BOOST_TEST_REQUIRE(!result.has_value()); - // Populate it - execution_state_access::complete(results_access::get_state(result), create_ok_packet(4, 1, 0, 3, "info")); - - // It's now valid + // With value + result = create_initial_results(); BOOST_TEST_REQUIRE(result.has_value()); - BOOST_TEST(result.meta().size() == 0u); - BOOST_TEST(result.rows().size() == 0u); - BOOST_TEST(result.affected_rows() == 4u); - BOOST_TEST(result.last_insert_id() == 1u); - BOOST_TEST(result.warning_count() == 3u); - BOOST_TEST(result.info() == "info"); } -results create_populated_results() -{ - auto st = create_execution_state(resultset_encoding::text, {protocol_field_type::var_string}); - execution_state_access::complete(st, create_ok_packet(2, 3, 0, 4, "small")); +BOOST_AUTO_TEST_SUITE(iterators) - results result; - results_access::get_rows(result) = makerows(1, "abc", nullptr); - results_access::get_state(result) = st; - return result; +BOOST_FIXTURE_TEST_CASE(basic, fixture) +{ + // Obtain iterators + auto it = result.begin(); // should point to resultset 0 + auto itend = result.end(); // should point to resultset 3 (1 past end) + + // Check dereference + BOOST_TEST((*it).info() == "1st"); + BOOST_TEST(it->info() == "1st"); + + // Check == + BOOST_TEST(!(it == itend)); + BOOST_TEST(!(itend == it)); + BOOST_TEST(it == result.begin()); + BOOST_TEST(it == it); + BOOST_TEST(itend == result.end()); + BOOST_TEST(itend == itend); + + // Check != + BOOST_TEST(it != itend); + BOOST_TEST(itend != it); + BOOST_TEST(!(it != result.begin())); + BOOST_TEST(!(it != it)); + BOOST_TEST(!(itend != result.end())); + BOOST_TEST(!(itend != itend)); } -BOOST_AUTO_TEST_CASE(move_constructor) +BOOST_FIXTURE_TEST_CASE(prefix_increment, fixture) { - // Construct a results object - auto result = create_populated_results(); + auto it = result.begin(); + auto& ref = (++it); + BOOST_TEST(&ref == &it); + BOOST_TEST(it->info() == "2nd"); + BOOST_TEST(it == result.begin() + 1); +} - // Obtain references +BOOST_FIXTURE_TEST_CASE(postfix_increment, fixture) +{ + auto it = result.begin(); + auto it2 = it++; + BOOST_TEST(it2 == result.begin()); + BOOST_TEST(it == result.begin() + 1); + BOOST_TEST(it->info() == "2nd"); +} + +BOOST_FIXTURE_TEST_CASE(prefix_decrement, fixture) +{ + auto it = result.end(); + auto& ref = (--it); + BOOST_TEST(&ref == &it); + BOOST_TEST(it->info() == "3rd"); + BOOST_TEST(it == result.begin() + 2); +} + +BOOST_FIXTURE_TEST_CASE(postfix_decrement, fixture) +{ + auto it = result.end(); + auto it2 = it--; + BOOST_TEST(it2 == result.end()); + BOOST_TEST(it == result.begin() + 2); + BOOST_TEST(it->info() == "3rd"); +} + +BOOST_FIXTURE_TEST_CASE(operator_square_brackets, fixture) +{ + auto it = result.begin(); + BOOST_TEST(it[0].info() == "1st"); + BOOST_TEST(it[1].info() == "2nd"); + BOOST_TEST(it[2].info() == "3rd"); +} + +BOOST_FIXTURE_TEST_CASE(operator_plus, fixture) +{ + auto it = result.begin(); + + // Increment by 1 + auto it2 = it + 1; + BOOST_TEST(it2->info() == "2nd"); + + // Reversed operands + it2 = 1 + it2; + BOOST_TEST(it2->info() == "3rd"); + + // Increment by more than 1 + BOOST_TEST(result.begin() + 3 == result.end()); + + // Increment by 0 + BOOST_TEST(result.begin() + 0 == result.begin()); + + // Negative increment + BOOST_TEST(result.end() + (-2) == result.begin() + 1); +} + +BOOST_FIXTURE_TEST_CASE(operator_plus_equals, fixture) +{ + auto it = result.begin(); + + // Increment by 1 + it += 1; + BOOST_TEST(it->info() == "2nd"); + + // Increment by more than + it += 2; + BOOST_TEST(it == result.end()); + + // Increment by 0 + it += 0; + BOOST_TEST(it == result.end()); + + // Negative increment + it += (-2); + BOOST_TEST(it == result.begin() + 1); +} + +BOOST_FIXTURE_TEST_CASE(operator_minus, fixture) +{ + auto it = result.end(); + + // Decrement by 1 + auto it2 = it - 1; + BOOST_TEST(it2->info() == "3rd"); + + // Decrement by more than 1 + BOOST_TEST(result.end() - 3 == result.begin()); + + // Decrement by 0 + BOOST_TEST(result.end() - 0 == result.end()); + + // Negative decrement + BOOST_TEST(result.begin() - (-2) == result.begin() + 2); +} + +BOOST_FIXTURE_TEST_CASE(operator_minus_equals, fixture) +{ + auto it = result.end(); + + // Decrement by 1 + it -= 1; + BOOST_TEST(it->info() == "3rd"); + + // Decrement by more than 1 + it -= 2; + BOOST_TEST(it == result.begin()); + + // Decrement by 0 + it -= 0; + BOOST_TEST(it == result.begin()); + + // Negative decrement + it -= (-2); + BOOST_TEST(it == result.begin() + 2); +} + +BOOST_FIXTURE_TEST_CASE(difference, fixture) +{ + auto first = result.begin(); + auto second = result.begin() + 1; + auto last = result.end(); + + BOOST_TEST(last - first == 3); + BOOST_TEST(last - second == 2); + BOOST_TEST(last - last == 0); + BOOST_TEST(first - last == -3); + BOOST_TEST(second - last == -2); + BOOST_TEST(last - last == 0); + BOOST_TEST(first - first == 0); +} + +BOOST_FIXTURE_TEST_CASE(relational, fixture) +{ + auto first = result.begin(); + auto second = result.begin() + 1; + auto third = result.begin() + 2; + + // Less than + BOOST_TEST(first < second); + BOOST_TEST(first <= second); + BOOST_TEST(!(first > second)); + BOOST_TEST(!(first >= second)); + + // Equal + BOOST_TEST(!(second < second)); + BOOST_TEST(second <= second); + BOOST_TEST(!(second > second)); + BOOST_TEST(second >= second); + + // Greater than + BOOST_TEST(!(third < second)); + BOOST_TEST(!(third <= second)); + BOOST_TEST(third > second); + BOOST_TEST(third >= second); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_CASE(collection_fns, fixture) +{ + // at + BOOST_TEST(result.at(0).info() == "1st"); + BOOST_TEST(result.at(1).info() == "2nd"); + BOOST_TEST(result.at(2).info() == "3rd"); + BOOST_CHECK_THROW(result.at(3), std::out_of_range); + + // operator[] + BOOST_TEST(result[0].info() == "1st"); + BOOST_TEST(result[1].info() == "2nd"); + BOOST_TEST(result[2].info() == "3rd"); + + // front & back + BOOST_TEST(result.front().info() == "1st"); + BOOST_TEST(result.back().info() == "3rd"); + + // size & empty + BOOST_TEST(result.size() == 3u); + BOOST_TEST(!result.empty()); +} + +// Verify view validity +BOOST_FIXTURE_TEST_CASE(move_constructor, fixture) +{ + // Obtain references. Note that iterators and resultset_view's don't remain valid. auto rws = result.rows(); auto meta = result.meta(); auto info = result.info(); @@ -77,23 +294,18 @@ BOOST_AUTO_TEST_CASE(move_constructor) // Make sure that views are still valid BOOST_TEST(rws == makerows(1, "abc", nullptr)); - BOOST_TEST_REQUIRE(meta.size() == 1u); - BOOST_TEST(meta[0].type() == column_type::varchar); - BOOST_TEST(info == "small"); + check_meta(meta, {column_type::varchar}); + BOOST_TEST(info == "1st"); // The new object holds the same data BOOST_TEST_REQUIRE(result2.has_value()); BOOST_TEST(result2.rows() == makerows(1, "abc", nullptr)); - BOOST_TEST_REQUIRE(result2.meta().size() == 1u); - BOOST_TEST(result2.meta()[0].type() == column_type::varchar); - BOOST_TEST(result2.info() == "small"); + check_meta(result2.meta(), {column_type::varchar}); + BOOST_TEST(result2.info() == "1st"); } -BOOST_AUTO_TEST_CASE(move_assignment) +BOOST_FIXTURE_TEST_CASE(move_assignment, fixture) { - // Construct a results object - auto result = create_populated_results(); - // Obtain references auto rws = result.rows(); auto meta = result.meta(); @@ -106,16 +318,14 @@ BOOST_AUTO_TEST_CASE(move_assignment) // Make sure that views are still valid BOOST_TEST(rws == makerows(1, "abc", nullptr)); - BOOST_TEST_REQUIRE(meta.size() == 1u); - BOOST_TEST(meta[0].type() == column_type::varchar); - BOOST_TEST(info == "small"); + check_meta(meta, {column_type::varchar}); + BOOST_TEST(info == "1st"); // The new object holds the same data BOOST_TEST_REQUIRE(result2.has_value()); BOOST_TEST(result2.rows() == makerows(1, "abc", nullptr)); - BOOST_TEST_REQUIRE(result2.meta().size() == 1u); - BOOST_TEST(result2.meta()[0].type() == column_type::varchar); - BOOST_TEST(result2.info() == "small"); + check_meta(result2.meta(), {column_type::varchar}); + BOOST_TEST(result2.info() == "1st"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/resultset.cpp b/test/unit/resultset.cpp new file mode 100644 index 00000000..40234386 --- /dev/null +++ b/test/unit/resultset.cpp @@ -0,0 +1,161 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include + +#include + +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message_struct.hpp" +#include "printing.hpp" +#include "test_common.hpp" + +using namespace boost::mysql::test; +using boost::mysql::column_type; +using boost::mysql::results; +using boost::mysql::resultset; +using boost::mysql::resultset_view; +using boost::mysql::detail::protocol_field_type; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_resultset) + +results create_initial_results() +{ + return create_results({ + {{protocol_field_type::var_string}, + makerows(1, "abc", nullptr), + ok_builder().affected_rows(1).last_insert_id(2).warnings(3).info("1st").build()}, + + {{protocol_field_type::tiny}, + makerows(1, 42), + ok_builder().affected_rows(4).last_insert_id(5).warnings(6).info("2nd").out_params(true).build()}, + }); +} + +BOOST_AUTO_TEST_CASE(default_ctor) +{ + resultset r; + BOOST_TEST(!r.has_value()); +} + +BOOST_AUTO_TEST_CASE(ctor_from_view_empty) +{ + resultset r{resultset_view{}}; + BOOST_TEST(!r.has_value()); +} + +BOOST_AUTO_TEST_CASE(ctor_from_view) +{ + results result = create_initial_results(); + resultset r{result.at(0)}; + result = results(); + + BOOST_TEST_REQUIRE(r.has_value()); + BOOST_TEST(r.rows() == makerows(1, "abc", nullptr)); + check_meta(r.meta(), {column_type::varchar}); + BOOST_TEST(r.affected_rows() == 1u); + BOOST_TEST(r.last_insert_id() == 2u); + BOOST_TEST(r.warning_count() == 3u); + BOOST_TEST(r.info() == "1st"); + BOOST_TEST(!r.is_out_params()); +} + +BOOST_AUTO_TEST_CASE(assignment_from_view_empty) +{ + results result = create_initial_results(); + resultset r{result.at(0)}; + r = resultset(); + result = results(); + + BOOST_TEST(!r.has_value()); +} + +BOOST_AUTO_TEST_CASE(assignment_from_view) +{ + results result = create_initial_results(); + resultset r{result.at(0)}; + r = result.at(1); + result = results(); + + BOOST_TEST_REQUIRE(r.has_value()); + BOOST_TEST(r.rows() == makerows(1, 42)); + check_meta(r.meta(), {column_type::tinyint}); + BOOST_TEST(r.affected_rows() == 4u); + BOOST_TEST(r.last_insert_id() == 5u); + BOOST_TEST(r.warning_count() == 6u); + BOOST_TEST(r.info() == "2nd"); + BOOST_TEST(r.is_out_params()); +} + +// View validity +BOOST_AUTO_TEST_CASE(move_constructor) +{ + // Construct object + results result = create_initial_results(); + resultset r1(result.at(0)); + + // Obtain references + auto rws = r1.rows(); + auto meta = r1.meta(); + auto info = r1.info(); + + // Move construct + resultset r2(std::move(r1)); + r1 = resultset(); + + // Make sure that views are still valid + BOOST_TEST(rws == makerows(1, "abc", nullptr)); + check_meta(meta, {column_type::varchar}); + BOOST_TEST(info == "1st"); + + // The new object holds the same data + BOOST_TEST_REQUIRE(r2.has_value()); + BOOST_TEST(r2.rows() == makerows(1, "abc", nullptr)); + check_meta(r2.meta(), {column_type::varchar}); + BOOST_TEST(r2.info() == "1st"); +} + +BOOST_AUTO_TEST_CASE(move_assignment) +{ + // Construct object + results result = create_initial_results(); + resultset r1(result.at(0)); + + // Obtain references + auto rws = r1.rows(); + auto meta = r1.meta(); + auto info = r1.info(); + + // Move construct + resultset r2; + r2 = std::move(r1); + r1 = resultset(); + + // Make sure that views are still valid + BOOST_TEST(rws == makerows(1, "abc", nullptr)); + BOOST_TEST_REQUIRE(meta.size() == 1u); + BOOST_TEST(meta[0].type() == column_type::varchar); + BOOST_TEST(info == "1st"); + + // The new object holds the same data + BOOST_TEST_REQUIRE(r2.has_value()); + BOOST_TEST(r2.rows() == makerows(1, "abc", nullptr)); + check_meta(r2.meta(), {column_type::varchar}); + BOOST_TEST(r2.info() == "1st"); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace diff --git a/test/unit/resultset_view.cpp b/test/unit/resultset_view.cpp new file mode 100644 index 00000000..922136cd --- /dev/null +++ b/test/unit/resultset_view.cpp @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include + +#include "check_meta.hpp" +#include "creation/create_execution_state.hpp" +#include "creation/create_message_struct.hpp" +#include "test_common.hpp" + +using namespace boost::mysql::test; +using boost::mysql::column_type; +using boost::mysql::resultset_view; +using boost::mysql::detail::protocol_field_type; + +namespace { + +BOOST_AUTO_TEST_SUITE(test_resultset_view) + +BOOST_AUTO_TEST_CASE(null_view) +{ + resultset_view v; + BOOST_TEST(!v.has_value()); +} + +BOOST_AUTO_TEST_CASE(valid_view) +{ + auto result = create_results({ + {{protocol_field_type::tiny}, + makerows(1, 42), + ok_builder().affected_rows(4).last_insert_id(5).warnings(6).info("2nd").out_params(true).build()} + }); + + auto v = result.at(0); + BOOST_TEST_REQUIRE(v.has_value()); + BOOST_TEST(v.rows() == makerows(1, 42)); + check_meta(v.meta(), {column_type::tinyint}); + BOOST_TEST(v.affected_rows() == 4u); + BOOST_TEST(v.last_insert_id() == 5u); + BOOST_TEST(v.warning_count() == 6u); + BOOST_TEST(v.info() == "2nd"); + BOOST_TEST(v.is_out_params()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace diff --git a/test/unit/row.cpp b/test/unit/row.cpp index e567d3df..00cfc6c2 100644 --- a/test/unit/row.cpp +++ b/test/unit/row.cpp @@ -78,7 +78,7 @@ BOOST_AUTO_TEST_CASE(default_ctor) BOOST_TEST(r.empty()); } -BOOST_AUTO_TEST_SUITE(ctor_from_row_view) +BOOST_AUTO_TEST_SUITE(ctor_from_frow_view) BOOST_AUTO_TEST_CASE(empty) { row_view v; @@ -86,34 +86,25 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(r.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - auto fields = make_fv_arr(42, 5.0f); - row r(makerowv(fields.data(), fields.size())); - - // Fields still valid even when the original source of the view changed - fields = make_fv_arr(90, 2.0); - BOOST_TEST(r.size() == 2u); - BOOST_TEST(r[0] == field_view(42)); - BOOST_TEST(r[1] == field_view(5.0f)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { std::string s1("test"), s2(""); blob b{0x00, 0xab, 0xf5}; - auto fields = make_fv_arr(s1, s2, 50, b); + auto fields = make_fv_arr(42, s1, 5.0f, b, s2); row r(makerowv(fields.data(), fields.size())); - // Fields still valid even when the original strings changed + // Fields still valid even when the original source of the view changed + fields = make_fv_arr(0, 0, 0, 0, 0); s1 = "other"; s2 = "abcdef"; b = {0xff, 0xa4, 0x02}; - BOOST_TEST(r.size() == 4u); - BOOST_TEST(r[0] == field_view("test")); - BOOST_TEST(r[1] == field_view("")); - BOOST_TEST(r[2] == field_view(50)); - BOOST_MYSQL_ASSERT_BLOB_EQUALS(r[3].as_blob(), blob({0x00, 0xab, 0xf5})); + + BOOST_TEST(r.size() == 5u); + BOOST_TEST(r[0] == field_view(42)); + BOOST_TEST(r[1] == field_view("test")); + BOOST_TEST(r[2] == field_view(5.0f)); + BOOST_TEST(r[3] == field_view(makebv("\0\xab\xf5"))); + BOOST_TEST(r[4] == field_view("")); } BOOST_AUTO_TEST_SUITE_END() @@ -127,18 +118,7 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(r2.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - row r1 = makerow(42, 5.0f); - row r2(r1); - r1 = makerow(42, "test"); // r2 should be independent of r1 - - BOOST_TEST(r2.size() == 2u); - BOOST_TEST(r2[0] == field_view(42)); - BOOST_TEST(r2[1] == field_view(5.0f)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { row r1 = makerow("", 42, "test", makebv("\0\3\2")); row r2(r1); @@ -167,23 +147,7 @@ BOOST_AUTO_TEST_CASE(empty) refcheck.check(r2); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - row r1 = makerow(42, 5.0f); - - // References, pointers, etc. should remain valid - reference_checker refcheck(r1); - - row r2(std::move(r1)); - r1 = makerow(42, "test"); // r2 should be independent of r1 - - BOOST_TEST(r2.size() == 2u); - BOOST_TEST(r2[0] == field_view(42)); - BOOST_TEST(r2[1] == field_view(5.0f)); - refcheck.check(r2); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { row r1 = makerow("", 42, "test", makebv("\0\5\xff")); @@ -215,20 +179,7 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(r1.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - row r1 = makerow(42, "abcdef"); - row r2 = makerow(50.0f, nullptr, 80u); - r1 = r2; - r2 = makerow("abc", 80, nullptr); // r1 is independent of r2 - - BOOST_TEST(r1.size() == 3u); - BOOST_TEST(r1[0] == field_view(50.0f)); - BOOST_TEST(r1[1] == field_view()); - BOOST_TEST(r1[2] == field_view(80u)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { row r1 = makerow(42, "abcdef", makebv("\0\1\2")); row r2 = makerow("a_very_long_string", nullptr, "", makebv("\3\4\5")); @@ -242,29 +193,7 @@ BOOST_AUTO_TEST_CASE(strings_blobs) BOOST_TEST(r1[3] == field_view(makebv("\3\4\5"))); } -BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) -{ - row r1; - row r2 = makerow("abc", nullptr, "bcd", makebv("\1\2\3")); - r1 = r2; - - BOOST_TEST(r1.size() == 4u); - BOOST_TEST(r1[0] == field_view("abc")); - BOOST_TEST(r1[1] == field_view()); - BOOST_TEST(r1[2] == field_view("bcd")); - BOOST_TEST(r1[3] == field_view(makebv("\1\2\3"))); -} - -BOOST_AUTO_TEST_CASE(self_assignment_empty) -{ - row r; - const row& ref = r; - r = ref; - - BOOST_TEST(r.empty()); -} - -BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +BOOST_AUTO_TEST_CASE(self_assignment) { row r = makerow("abc", 50u, "fgh"); const row& ref = r; @@ -289,25 +218,7 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(rv == r1); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - row r1 = makerow(42, "abcdef"); - row r2 = makerow(50.0f, nullptr, 80u); - - // References, pointers, etc should remain valid - reference_checker refcheck(r2); - - r1 = std::move(r2); - r2 = makerow("abc", 80, nullptr); // r1 is independent of r2 - - BOOST_TEST(r1.size() == 3u); - BOOST_TEST(r1[0] == field_view(50.0f)); - BOOST_TEST(r1[1] == field_view()); - BOOST_TEST(r1[2] == field_view(80u)); - refcheck.check(r1); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { row r1 = makerow(42, "abcdef", makebv("\0\4\1")); row r2 = makerow("a_very_long_string", nullptr, "", makebv("\7\1\2")); @@ -329,37 +240,7 @@ BOOST_AUTO_TEST_CASE(strings_blobs) refcheck.check(r1); } -BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) -{ - row r1; - row r2 = makerow("abc", nullptr, "bcd", makebv("\0\2\5")); - - // References, pointers, etc should remain valid - reference_checker_strs refcheck(r2, 2, 3); - - r1 = std::move(r2); - - BOOST_TEST(r1.size() == 4u); - BOOST_TEST(r1[0] == field_view("abc")); - BOOST_TEST(r1[1] == field_view()); - BOOST_TEST(r1[2] == field_view("bcd")); - BOOST_TEST(r1[3] == field_view(makebv("\0\2\5"))); - refcheck.check(r1); -} - -BOOST_AUTO_TEST_CASE(self_assignment_empty) -{ - row r; - row&& ref = std::move(r); - r = std::move(ref); - - // r is in a valid but unspecified state; can be assigned to - r = makerow("abcdef"); - BOOST_TEST(r.size() == 1u); - BOOST_TEST(r[0] == field_view("abcdef")); -} - -BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +BOOST_AUTO_TEST_CASE(self_assignment) { row r = makerow("abc", 50u, "fgh", makebv("\0\4")); row&& ref = std::move(r); @@ -376,23 +257,11 @@ BOOST_AUTO_TEST_SUITE(assignment_from_view) BOOST_AUTO_TEST_CASE(empty) { row r = makerow(42, "abcdef", makebv("\0\xae")); - r = row(); + r = row_view(); BOOST_TEST(r.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - row r = makerow(42, "abcdef"); - auto fields = make_fv_arr(90, nullptr); - r = makerowv(fields.data(), fields.size()); - fields = make_fv_arr("abc", 42u); // r should be independent of the original fields - - BOOST_TEST(r.size() == 2u); - BOOST_TEST(r[0] == field_view(90)); - BOOST_TEST(r[1] == field_view()); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { std::string s1("a_very_long_string"), s2(""); blob b{0x00, 0xfa}; @@ -411,19 +280,6 @@ BOOST_AUTO_TEST_CASE(strings_blobs) BOOST_TEST(r[3] == field_view(makebv("\0\xfa"))); } -BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) -{ - row r; - auto fields = make_fv_arr("abc", nullptr, "bcd", makebv("\0\3")); - r = makerowv(fields.data(), fields.size()); - - BOOST_TEST(r.size() == 4u); - BOOST_TEST(r[0] == field_view("abc")); - BOOST_TEST(r[1] == field_view()); - BOOST_TEST(r[2] == field_view("bcd")); - BOOST_TEST(r[3] == field_view(makebv("\0\3"))); -} - BOOST_AUTO_TEST_CASE(self_assignment) { row r = makerow("abcdef", 42, "plk"); diff --git a/test/unit/rows.cpp b/test/unit/rows.cpp index 5cc1b7fc..e12d3d7e 100644 --- a/test/unit/rows.cpp +++ b/test/unit/rows.cpp @@ -99,19 +99,7 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(r.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - auto fields = make_fv_arr(20u, 1.0f, nullptr, -1); - auto v = makerowsv(fields.data(), fields.size(), 2); - rows r(v); - fields = make_fv_arr(0, 0, 0, 0); // r should be independent of the original fields - - BOOST_TEST(r.size() == 2u); - BOOST_TEST(r[0] == makerow(20u, 1.0f)); - BOOST_TEST(r[1] == makerow(nullptr, -1)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { std::string s1("abc"), s2(""); blob b{0x64, 0x10, 0x01}; @@ -139,18 +127,7 @@ BOOST_AUTO_TEST_CASE(empty) BOOST_TEST(r2.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - rows r1 = makerows(3, 1, 21.0f, nullptr, 2, 22.0f, -1); - rows r2(r1); - r1 = makerows(2, 0, 0, 0, 0); // r2 should be independent of r1 - - BOOST_TEST(r2.size() == 2u); - BOOST_TEST(r2[0] == makerow(1, 21.0f, nullptr)); - BOOST_TEST(r2[1] == makerow(2, 22.0f, -1)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { rows r1 = makerows(3, "abc", 21.0f, makebv(""), "cdefg", 22.0f, makebv("\1\3\5")); rows r2(r1); @@ -177,23 +154,7 @@ BOOST_AUTO_TEST_CASE(empty) refcheck.check(r2); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - rows r1 = makerows(3, 1, 21.0f, nullptr, 2, 22.0f, -1); - - // References, pointers, etc. should remain valid - reference_checker refcheck(r1); - - rows r2(std::move(r1)); - r1 = makerows(2, 0, 0, 0, 0); // r2 should be independent of r1 - - BOOST_TEST(r2.size() == 2u); - BOOST_TEST(r2[0] == makerow(1, 21.0f, nullptr)); - BOOST_TEST(r2[1] == makerow(2, 22.0f, -1)); - refcheck.check(r2); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { rows r1 = makerows(3, makebv("abc"), 21.0f, "", makebv("\0\1\0"), 22.0f, "aaa"); @@ -211,16 +172,7 @@ BOOST_AUTO_TEST_CASE(strings_blobs) BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(copy_assignment) -BOOST_AUTO_TEST_CASE(empty_to_empty) -{ - rows r1; - rows r2; - r1 = r2; - r2 = makerows(2, 90, nullptr); // r1 is independent of r2 - BOOST_TEST(r1.empty()); -} - -BOOST_AUTO_TEST_CASE(empty_to_nonempty) +BOOST_AUTO_TEST_CASE(empty) { rows r1 = makerows(2, 42, "abcdef"); rows r2; @@ -229,19 +181,7 @@ BOOST_AUTO_TEST_CASE(empty_to_nonempty) BOOST_TEST(r1.empty()); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - rows r1 = makerows(2, 42, "abcdef"); - rows r2 = makerows(1, 50.0f, nullptr); - r1 = r2; - r2 = makerows(1, "abc", 80, nullptr); // r1 is independent of r2 - - BOOST_TEST(r1.size() == 2u); - BOOST_TEST(r1[0] == makerow(50.0f)); - BOOST_TEST(r1[1] == makerow(nullptr)); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { rows r1 = makerows(1, 42, "abcdef", makebv("\0\2\1")); rows r2 = makerows(2, "a_very_long_string", nullptr, "", makebv("\7\0"), "cde", blob_view()); @@ -254,29 +194,7 @@ BOOST_AUTO_TEST_CASE(strings_blobs) BOOST_TEST(r1[2] == makerow("cde", blob_view())); } -BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) -{ - rows r1; - rows r2 = makerows(1, "abc", nullptr, "", makebv("\0\1\7")); - r1 = r2; - - BOOST_TEST(r1.size() == 4u); - BOOST_TEST(r1[0] == makerow("abc")); - BOOST_TEST(r1[1] == makerow(nullptr)); - BOOST_TEST(r1[2] == makerow("")); - BOOST_TEST(r1[3] == makerow(makebv("\0\1\7"))); -} - -BOOST_AUTO_TEST_CASE(self_assignment_empty) -{ - rows r; - const rows& ref = r; - r = ref; - - BOOST_TEST(r.empty()); -} - -BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +BOOST_AUTO_TEST_CASE(self_assignment) { rows r = makerows(2, "abc", 50u, "", makebv("abc")); const rows& ref = r; @@ -289,21 +207,7 @@ BOOST_AUTO_TEST_CASE(self_assignment_non_empty) BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(move_assignment) -BOOST_AUTO_TEST_CASE(empty_to_empty) -{ - rows r1; - rows r2; - - // References, pointers, etc. should remain valid - reference_checker refcheck(r2); - r1 = std::move(r2); - r2 = makerows(1, 20, 20); // r1 independent of r2 - - BOOST_TEST(r1.empty()); - refcheck.check(r1); -} - -BOOST_AUTO_TEST_CASE(empty_to_nonempty) +BOOST_AUTO_TEST_CASE(empty) { rows r1 = makerows(1, 42, "abcdef"); rows r2; @@ -317,23 +221,7 @@ BOOST_AUTO_TEST_CASE(empty_to_nonempty) refcheck.check(r1); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - rows r1 = makerows(2, 42, "abcdef"); - rows r2 = makerows(3, 50.0f, nullptr, 80u); - - // References, pointers, etc. should remain valid - reference_checker refcheck(r2); - - r1 = std::move(r2); - r2 = makerows(1, "abc", 80, nullptr); // r1 is independent of r2 - - BOOST_TEST(r1.size() == 1u); - BOOST_TEST(r1[0] == makerow(50.0f, nullptr, 80u)); - refcheck.check(r1); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { rows r1 = makerows(1, 42, "abcdef"); rows r2 = makerows(3, "a_very_long_string", blob_view(), 50, "", makebv("\2\5\1"), 42); @@ -350,37 +238,7 @@ BOOST_AUTO_TEST_CASE(strings_blobs) refcheck.check(r1); } -BOOST_AUTO_TEST_CASE(strings_blobs_empty_to) -{ - rows r1; - rows r2 = makerows(1, "abc", makebv("\0\4"), "bcd"); - - // References, pointers, etc. should remain valid - reference_checker_strs refcheck(r2, 0, 0, 1, 0); - - r1 = std::move(r2); - r2 = rows(); // r1 independent of r2 - - BOOST_TEST(r1.size() == 3u); - BOOST_TEST(r1[0] == makerow("abc")); - BOOST_TEST(r1[1] == makerow(makebv("\0\4"))); - BOOST_TEST(r1[2] == makerow("bcd")); - refcheck.check(r1); -} - -BOOST_AUTO_TEST_CASE(self_assignment_empty) -{ - rows r; - rows&& ref = std::move(r); - r = std::move(ref); - - // r is in a valid but unspecified state; can be assigned to - r = makerows(1, "abcdef"); - BOOST_TEST(r.size() == 1u); - BOOST_TEST(r[0] == makerow("abcdef")); -} - -BOOST_AUTO_TEST_CASE(self_assignment_non_empty) +BOOST_AUTO_TEST_CASE(self_assignment) { rows r = makerows(3, "abc", 50u, "fgh"); rows&& ref = std::move(r); @@ -394,15 +252,7 @@ BOOST_AUTO_TEST_CASE(self_assignment_non_empty) BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(assignment_from_view) -BOOST_AUTO_TEST_CASE(empty_to_empty) -{ - rows r; - r = rows_view(); - BOOST_TEST(r.empty()); - BOOST_TEST(r.num_columns() == 0u); -} - -BOOST_AUTO_TEST_CASE(empty_to_nonempty) +BOOST_AUTO_TEST_CASE(empty) { rows r = makerows(1, 42, "abcdef"); r = rows_view(); @@ -426,19 +276,7 @@ BOOST_AUTO_TEST_CASE(empty_different_num_columns) BOOST_TEST(r.num_columns() == 3u); } -BOOST_AUTO_TEST_CASE(non_strings) -{ - rows r = makerows(1, 42, "abcdef"); - auto fields = make_fv_arr(90, nullptr, 4.2f, 1u); - r = makerowsv(fields.data(), fields.size(), 2); - - BOOST_TEST_REQUIRE(r.size() == 2u); - BOOST_TEST(r[0] == makerow(90, nullptr)); - BOOST_TEST(r[1] == makerow(4.2f, 1u)); - BOOST_TEST(r.num_columns() == 2u); -} - -BOOST_AUTO_TEST_CASE(strings_blobs) +BOOST_AUTO_TEST_CASE(non_empty) { std::string s1("a_very_long_string"), s2(""); blob b{0x78, 0x01, 0xff}; @@ -456,16 +294,6 @@ BOOST_AUTO_TEST_CASE(strings_blobs) BOOST_TEST(r.num_columns() == 3u); } -BOOST_AUTO_TEST_CASE(strings_empty_to) -{ - rows r; - auto fields = make_fv_arr("abc", nullptr, "bcd", 8.2f); - r = makerowsv(fields.data(), fields.size(), 4); - - BOOST_TEST_REQUIRE(r.size() == 1u); - BOOST_TEST(r[0] == makerow("abc", nullptr, "bcd", 8.2f)); -} - BOOST_AUTO_TEST_CASE(self_assignment) { rows r = makerows(2, "abcdef", 42, "plk", "uv"); @@ -475,24 +303,6 @@ BOOST_AUTO_TEST_CASE(self_assignment) BOOST_TEST(r[0] == makerow("abcdef", 42)); BOOST_TEST(r[1] == makerow("plk", "uv")); } - -BOOST_AUTO_TEST_CASE(self_assignment_empty) -{ - rows r; - r = rows_view(r); - - BOOST_TEST(r.empty()); - BOOST_TEST(r.size() == 0u); -} - -BOOST_AUTO_TEST_CASE(self_assignment_cleared) -{ - rows r = makerows(2, "abcdef", 42, "plk", "uv"); - r = rows(); - r = rows_view(r); - - BOOST_TEST_REQUIRE(r.size() == 0u); -} BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(at) diff --git a/test/unit/statement.cpp b/test/unit/statement.cpp index dc0ae04e..1eb7a9d0 100644 --- a/test/unit/statement.cpp +++ b/test/unit/statement.cpp @@ -9,7 +9,7 @@ #include -#include "create_statement.hpp" +#include "creation/create_statement.hpp" #include "test_common.hpp" using namespace boost::mysql::test; @@ -27,7 +27,7 @@ BOOST_AUTO_TEST_CASE(default_ctor) BOOST_AUTO_TEST_CASE(member_fns) { - auto stmt = create_statement(3, 1); + auto stmt = statement_builder().id(1).num_params(3).build(); BOOST_TEST(stmt.valid()); BOOST_TEST(stmt.num_params() == 3u); diff --git a/test/unit/throw_on_error.cpp b/test/unit/throw_on_error.cpp index 039540f8..f94e53cb 100644 --- a/test/unit/throw_on_error.cpp +++ b/test/unit/throw_on_error.cpp @@ -14,7 +14,7 @@ #include #include -#include "create_diagnostics.hpp" +#include "creation/create_diagnostics.hpp" using boost::mysql::error_code; using boost::mysql::error_with_diagnostics; diff --git a/tools/ci.py b/tools/ci.py index 67dae29f..ad411bb8 100644 --- a/tools/ci.py +++ b/tools/ci.py @@ -95,7 +95,7 @@ def _install_boost( copytree( str(source_dir), str(lib_dir), - ignore=ignore_patterns('__build*__'), + ignore=ignore_patterns('__build*__', '.git'), **({ 'dirs_exist_ok': True } if _supports_dir_exist_ok else {}) ) @@ -137,6 +137,7 @@ def _db_setup( db: str = 'mysql8' ) -> None: _run_sql_file(source_dir.joinpath('example', 'db_setup.sql')) + _run_sql_file(source_dir.joinpath('example', 'db_setup_stored_procedures.sql')) _run_sql_file(source_dir.joinpath('test', 'integration', 'db_setup.sql')) if db == 'mysql8': _run_sql_file(source_dir.joinpath('test', 'integration', 'db_setup_sha256.sql')) @@ -210,7 +211,7 @@ def _b2_build( '-j4', 'libs/mysql/test', 'libs/mysql/test/integration//boost_mysql_integrationtests', - 'libs/mysql/example//boost_mysql_all_examples' + 'libs/mysql/example' ]) diff --git a/tools/scripts/build_unix_local.sh b/tools/scripts/build_unix_local.sh index a31321c3..03777203 100755 --- a/tools/scripts/build_unix_local.sh +++ b/tools/scripts/build_unix_local.sh @@ -28,7 +28,7 @@ docker exec $CONTAINER python /opt/boost-mysql/tools/ci.py --source-dir=/opt/boo --coverage=0 \ --clean=0 \ --toolset=gcc \ - --cxxstd=17 \ + --cxxstd=20 \ --variant=debug \ --cmake-standalone-tests=1 \ --cmake-add-subdir-tests=1 \