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 @@
+
+
+
+
\ 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_paramsmetadataresults
+ resultset_view
+ resultsetrowrow_viewrows
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