diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk
index ba6e34a6f..520e2d3e9 100644
--- a/doc/qbk/00_main.qbk
+++ b/doc/qbk/00_main.qbk
@@ -31,7 +31,7 @@
[template reflink[id][reflink2 [id] [id]]]
[template refmem[class mem][reflink2 [class].[mem] [class]::[mem]]]
[template refmemunq[class mem][reflink2 [class].[mem] [mem]]]
-[template asioreflink[id term][@boost:/doc/html/boost_asio/reference/[id].html [^boost::asio::[term]]]]
+[template asioreflink[id term][@boost:/doc/html/boost_asio/reference/[id].html [^asio::[term]]]]
[template mysqllink[id text][@https://dev.mysql.com/doc/refman/8.0/en/[id] [text]]]
[def __CompletionToken__ [@boost:/doc/html/boost_asio/reference/asynchronous_operations.html#boost_asio.reference.asynchronous_operations.completion_tokens_and_handlers ['CompletionToken]]]
@@ -129,9 +129,12 @@ END
[import ../../example/1_tutorial/2_async.cpp]
[import ../../example/1_tutorial/3_with_params.cpp]
[import ../../example/1_tutorial/4_static_interface.cpp]
+[import ../../example/1_tutorial/5_updates_transactions.cpp]
+[import ../../example/1_tutorial/6_connection_pool.cpp]
+[import ../../example/1_tutorial/7_error_handling.cpp]
+[import ../../example/2_simple/inserts.cpp]
+[import ../../example/2_simple/deletes.cpp]
[import ../../example/2_simple/prepared_statements.cpp]
-[import ../../example/2_simple/timeouts.cpp]
-[import ../../example/2_simple/multi_queries_transactions.cpp]
[import ../../example/2_simple/disable_tls.cpp]
[import ../../example/2_simple/tls_certificate_verification.cpp]
[import ../../example/2_simple/metadata.cpp]
@@ -160,6 +163,7 @@ END
[import ../../test/integration/test/snippets/sql_formatting_custom.cpp]
[import ../../test/integration/test/snippets/multi_function.cpp]
[import ../../test/integration/test/snippets/tutorials.cpp]
+[import ../../test/integration/test/snippets/templated_connection.cpp]
[import ../../test/integration/test/snippets/metadata.cpp]
[import ../../test/integration/test/snippets/connection_pool.cpp]
[import ../../test/integration/test/snippets/time_types.cpp]
@@ -178,6 +182,9 @@ END
[include 03_2_tutorial_async.qbk]
[include 03_3_tutorial_with_params.qbk]
[include 03_4_tutorial_static_interface.qbk]
+[include 03_5_tutorial_updates_transactions.qbk]
+[include 03_6_tutorial_connection_pool.qbk]
+[include 03_7_tutorial_error_handling.qbk]
[include 04_overview.qbk]
[include 05_connection_establishment.qbk]
[include 06_sql_formatting.qbk]
@@ -193,8 +200,7 @@ END
[include 16_metadata.qbk]
[include 17_charsets.qbk]
[include 18_time_types.qbk]
-[/ TODO: re-enable this
-[include 19_templated_connection.qbk] ]
+[include 19_templated_connection.qbk]
[include 20_pipeline.qbk]
[include 21_examples.qbk]
diff --git a/doc/qbk/03_4_tutorial_static_interface.qbk b/doc/qbk/03_4_tutorial_static_interface.qbk
index 89245d629..2496e773e 100644
--- a/doc/qbk/03_4_tutorial_static_interface.qbk
+++ b/doc/qbk/03_4_tutorial_static_interface.qbk
@@ -101,8 +101,6 @@ The mechanics are quite similar to what's been explained here.
Full program listing for this tutorial is [link mysql.examples.tutorial_static_interface here].
-This concludes our tutorial series. You can now look at the [link mysql.overview overview section]
-to learn more about the library features, or to the [link mysql.examples example section]
-if you prefer to learn by doing.
+You can now proceed to [link mysql.tutorial_updates_transactions the next tutorial].
[endsect]
\ No newline at end of file
diff --git a/doc/qbk/03_5_tutorial_updates_transactions.qbk b/doc/qbk/03_5_tutorial_updates_transactions.qbk
new file mode 100644
index 000000000..64ac871fb
--- /dev/null
+++ b/doc/qbk/03_5_tutorial_updates_transactions.qbk
@@ -0,0 +1,149 @@
+[/
+ Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+
+ Distributed under the Boost Software License, Version 1.0. (See accompanying
+ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+]
+
+[section:tutorial_updates_transactions Tutorial 5: UPDATEs, transactions and semicolon-separated queries]
+
+All the previous tutorials have only used `SELECT` statements, but
+Boost.MySQL is not limited to them. Using [refmemunq any_connection async_execute]
+you can run any SQL statement supported by MySQL.
+
+In this tutorial, we will write a program that changes the first name of an
+employee, given their ID, and prints the updated employee details.
+We will use an `UPDATE` and transaction management statements.
+`INSERT` and `DELETE` statements have similar mechanics.
+
+
+
+[heading A simple UPDATE]
+
+We can use the same tools and functions as in previous tutorials:
+
+[tutorial_updates_transactions_update]
+
+By default, auto-commit is enabled, meaning that when `async_execute`
+returns, the `UPDATE` is visible to other client connections.
+
+
+
+
+[heading Checking that the UPDATE took effect]
+
+The above query will succeed even if there was no employee with the given ID.
+We would like to retrieve the updated employee details on success, and emit
+a useful error message if no employee was matched.
+
+We may be tempted to use [refmem results affected_rows] at first, but
+this doesn't convey the information we're looking for:
+a row may be matched but not affected. For example, if you try to
+set `first_name` to the same value it already has,
+MySQL will count the row as a matched, but not affected.
+
+
+MySQL does not support the `UPDATE ... RETURNING` syntax, so we will
+have to retrieve the employee manually after updating it.
+We can add the following after our `UPDATE`:
+
+[tutorial_updates_transactions_select]
+
+However, the code above contains a race condition. Imagine the following situation:
+
+* The `UPDATE` is issued. No employee is matched.
+* Before our program sends the `SELECT` query, a different program inserts
+ an employee with the ID that we're trying to update.
+* Our program runs the `SELECT` statement and retrieves the newly inserted row.
+
+To our program, it looks like we succeeded performing the update, when
+we really didn't. Depending on the nature of our program, this may
+or may not have serious consequences, but it's something we should avoid.
+
+
+[heading Avoiding the race condition with a transaction block]
+
+We can fix the race condition using transactions.
+In MySQL, a transaction block is opened with `START TRANSACTION`.
+Subsequent statements will belong to the transaction block,
+until the transaction either commits or is rolled back.
+A `COMMIT` statement commits the transaction.
+A rollback happens if the connection that initiated the transaction
+closes or an explicit `ROLLBACK` statement is used.
+
+We will enclose our `UPDATE` and `SELECT` statements in
+a transaction block. This will ensure that the `SELECT`
+will get the updated row, if any:
+
+[tutorial_updates_transactions_txn]
+
+
+
+
+[heading Using multi-queries]
+
+While the code we've written is correct, it's not very performant.
+We're incurring in 4 round-trips to the server, when our queries don't depend
+on the result of previous ones. The round-trips occur within a transaction
+block, which causes certain database rows to be locked, increasing contention.
+We can improve the situation by running our four statements in a single batch.
+
+Multi-queries are a protocol feature that lets you execute several queries
+at once. Individual queries must be separated by semicolons.
+
+Multi-queries are disabled by default. To enable them, set
+[refmem connect_params multi_queries] to `true` before connecting:
+
+[tutorial_updates_transactions_connect]
+
+Multi-queries can be composed an executed using the same
+functions we've been using:
+
+[tutorial_updates_transactions_multi_queries]
+
+Accessing the results is slightly different. MySQL returns 4 resultsets,
+one for each query. In Boost.MySQL, this operation is said to be
+[link mysql.multi_resultset multi-resultset].
+[reflink results] can actually store more than one resultset.
+[refmem results rows] actually accesses the rows in the first resultset,
+because it's the most common use case.
+
+We want to get the rows retrieved by the `SELECT` statement,
+which corresponds to the third resultset.
+[refmem results at] returns a [reflink resultset_view] containing data
+for the requested resultset:
+
+[tutorial_updates_transactions_dynamic_results]
+
+
+[heading Using manual indices in with_params]
+
+Repeating `employee_id` in the parameter list passed to `with_params`
+violates the DRY principle.
+As with `std::format`, we can refer to a format argument more than once
+by using manual indices:
+
+[tutorial_updates_transactions_manual_indices]
+
+
+
+[heading Using the static interface with multi-resultset]
+
+Finally, we can rewrite our code to use the static interface so it's safer.
+In multi-resultset scenarios, we can pass as many row types
+to [reflink static_results] as resultsets we expect.
+We can use the empty tuple (`std::tuple<>`) as a row type
+for operations that don't return rows, like the `UPDATE`.
+Our code becomes:
+
+[tutorial_updates_transactions_static]
+
+
+[heading Wrapping up]
+
+Full program listing for this tutorial is [link mysql.examples.tutorial_updates_transactions here].
+
+You can now proceed to [link mysql.tutorial_connection_pool the next tutorial].
+
+
+[endsect]
\ No newline at end of file
diff --git a/doc/qbk/03_6_tutorial_connection_pool.qbk b/doc/qbk/03_6_tutorial_connection_pool.qbk
new file mode 100644
index 000000000..6533a4ac2
--- /dev/null
+++ b/doc/qbk/03_6_tutorial_connection_pool.qbk
@@ -0,0 +1,150 @@
+[/
+ Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+
+ Distributed under the Boost Software License, Version 1.0. (See accompanying
+ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+]
+
+[section:tutorial_connection_pool Tutorial 6: Connection pools]
+
+All our programs until now have used one-shot connections.
+They also didn't feature any fault tolerance:
+if the server is unavailable, our program throws an exception
+and terminates. Most real world scenarios require
+long-lived, reliable connections, instead.
+
+In this tutorial, we will implement a server for a simple request-reply
+protocol. The protocol allows clients to retrieve the full name of
+an employee given their ID.
+We will use [reflink connection_pool] to maintain a set of healthy connections
+that we can use when a client connects to our server.
+
+
+
+
+[heading The protocol]
+
+The protocol is TCP based, and can be described as follows:
+
+* After connecting, the client sends a message containing the employee ID,
+ encoded as an 8-byte, big-endian integer.
+* The server replies with a string containing the employee full name,
+ or "NOT_FOUND", if the ID doesn't match any employee.
+* The connection is closed after that.
+
+This protocol is intentionally overly simplistic, and
+shouldn't be used in production. See our
+[link mysql.examples.connection_pool HTTP examples]
+for more advanced use cases.
+
+
+
+
+[heading Creating a connection pool]
+
+[reflink connection_pool] is an I/O object that contains
+[reflink any_connection] objects, and can be
+constructed from an execution context and a [reflink pool_params]
+config struct:
+
+[tutorial_connection_pool_create]
+
+A single connection pool is usually created per application.
+
+[refmem connection_pool async_run] should be called once per pool:
+
+[tutorial_connection_pool_run]
+
+
+
+
+
+
+[heading Using pooled connections]
+
+Let's first write a coroutine that encapsulates database access.
+Given an employee ID, it should return the string to be sent as response to the client.
+Don't worry about error handling for now - we will take care of it in the next tutorial.
+
+When using a pool, we don't need to explicitly create, connect or close connections.
+Instead, we use [refmem connection_pool async_get_connection] to obtain them from the pool:
+
+[tutorial_connection_pool_get_connection]
+
+[reflink pooled_connection] is a wrapper around [reflink any_connection],
+with some pool-specific additions. We can use it like a regular connection:
+
+[tutorial_connection_pool_use]
+
+When a [reflink pooled_connection] is destroyed, the connection is returned
+to the pool. The underlying connection will be cleaned up using a lightweight
+session reset mechanism and recycled.
+Subsequent [refmemunq connection_pool async_get_connection]
+calls may retrieve the same connection. This improves efficiency,
+since session establishment is costly.
+
+[refmemunq connection_pool async_get_connection] waits
+for a client connection to become available before completing.
+If the server is unavailable or credentials are invalid,
+it may wait indefinitely. This is a problem for both development and production.
+We can solve this by using [asioreflink cancel_after cancel_after],
+which allows setting timeouts to async operations:
+
+[tutorial_connection_pool_get_connection_timeout]
+
+Don't worry if you don't fully understand how this works.
+We will go into more detail on [asioreflink cancel_after cancel_after],
+cancellations and completion tokens in the next tutorial.
+
+Putting all pieces together, our coroutine becomes:
+
+[tutorial_connection_pool_db]
+
+
+
+
+[heading Handling a client session]
+
+Let's now build a function that handles a client sessions,
+invoking the database access logic in the process:
+
+[tutorial_connection_pool_session]
+
+
+
+
+[heading Listening for connections]
+
+We now need logic to accept incoming TCP connections.
+We will use an `asio::ip::tcp::acceptor` object
+to accomplish it, listening for connections in a loop
+until the server is stopped:
+
+[tutorial_connection_pool_listener]
+
+
+
+
+[heading Waiting for signals]
+
+Finally, we need a way to stop our program. We will use an `asio::signal_set` object
+to catch signals, and call `io_context::stop` when Ctrl-C is pressed:
+
+[tutorial_connection_pool_signals]
+
+Putting all these pieces together, our main program becomes:
+
+[tutorial_connection_pool_main]
+
+
+
+
+[heading Wrapping up]
+
+Full program listing for this tutorial is [link mysql.examples.tutorial_connection_pool here].
+
+For simplicity, we've left error handling out of this tutorial.
+This is usually very important in a server like the one we've written,
+and is the topic of our [link mysql.tutorial_error_handling next tutorial].
+
+[endsect]
\ No newline at end of file
diff --git a/doc/qbk/03_7_tutorial_error_handling.qbk b/doc/qbk/03_7_tutorial_error_handling.qbk
new file mode 100644
index 000000000..c80737428
--- /dev/null
+++ b/doc/qbk/03_7_tutorial_error_handling.qbk
@@ -0,0 +1,246 @@
+[/
+ Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+
+ Distributed under the Boost Software License, Version 1.0. (See accompanying
+ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+]
+
+[section:tutorial_error_handling Tutorial 7: Error handling]
+
+The [link mysql.tutorial_connection_pool previous tutorial]
+did not include any error handling. When an error is encountered
+while talking to the DB or the client, an exception is thrown and
+the program terminates. This is undesirable in server programs like the one we're writing.
+
+To add error handling, we can just add try/catch blocks to prevent exception propagation.
+However, many code bases discourage the use of exceptions for non-exceptional circumstances,
+like I/O errors. In this tutorial, we will learn how to manage I/O errors without exceptions
+by using [asioreflink as_tuple as_tuple] and error codes.
+
+
+[heading Error handling strategy]
+
+There are two kind of I/O errors that our program can encounter:
+
+* Reading and writing to the client may fail. This can happen if
+ the client program is faulty or a network error happens.
+ In this case, we should log the problem and close the connection.
+* Talking to the database may fail. This can happen if [refmemunq connection_pool async_get_connection]
+ is cancelled because of a timeout. In this case, we will return a special string (`"ERROR"`)
+ to the client, signalling that we can't fulfill the request, and log the problem.
+
+Additionally, we will modify how we use `asio::cancel_after` to make the system more reliable.
+
+
+
+
+[heading Completion tokens]
+
+Before proceeding, we need to understand what a completion token is.
+The concepts in this section are not specific to Boost.MySQL, but apply
+to Asio and all Asio-compatible libraries. Since Asio docs can be terse,
+we explain them here to facilitate the reader.
+
+All asynchronous operations accept an optional, last parameter specifying what
+to do when the operation completes.
+This last parameter is the operation's
+[@boost:/doc/html/boost_asio/reference/asynchronous_operations.html#boost_asio.reference.asynchronous_operations.completion_tokens_and_handlers completion token].
+
+Callbacks are valid completion tokens. Taking [refmemunq connection_pool async_get_connection]
+as example, the following is valid:
+
+[tutorial_error_handling_callbacks]
+
+We have already been using this when creating coroutines.
+`asio::co_spawn` is also an async operation, and the callback we pass
+as its last parameter is the completion token.
+
+You might consider using callbacks if your compiler doesn't support coroutines,
+or just by personal preference. [link mysql.examples.callbacks This example]
+demonstrates how to use them.
+
+If you don't specify a completion token, the operation's [*default completion token]
+will be used. This is usually `asio::deferred` or `mysql::with_diagnostics(asio::deferred)`
+[footnote [reflink with_diagnostics] is an adapter completion token that enhances
+thrown exceptions with a diagnostic string supplied by the server.
+`mysql::with_diagnostics(asio::deferred)` is otherwise equivalent to `asio::deferred`.].
+These tokens transform asynchronous operations into awaitables,
+so we can use them in C++20 coroutines.
+
+The default completion token for [refmemunq connection_pool async_get_connection] is
+`mysql::with_diagnostics(asio::deferred)`. This means that the following two are equivalent:
+
+[tutorial_error_handling_default_tokens]
+
+Completion tokens are generic: once you learn how to use one, you can use it
+with any Asio-compliant async operation. This includes all functions in Boost.Asio,
+Boost.MySQL, Boost.Beast and Boost.Redis. We say that operations in these libraries
+are compliant with Asio's universal async model. Writing these is hard, but they're easy to use!
+
+
+
+
+
+[heading Adapter completion tokens]
+
+Some tokens don't fully specify what to do when the operation completes,
+but rather modify some aspect of how the operation executes.
+They wrap (or adapt) other completion tokens. The underlying token
+determines what to do when the operation completes.
+
+[asioreflink cancel_after cancel_after] is an adapter token.
+It modifies how an operation executes by setting a timeout,
+but it doesn't specify what to do on completion.
+
+Adapter tokens can be passed an optional completion token
+as the last argument. If the token is omitted, the default
+one will be used. Continuing with our example:
+
+[tutorial_error_handling_adapter_tokens]
+
+
+[heading Handler signature and exceptions]
+
+Each async operation has an associated handler signature.
+We can find these signatures in the documentation for each operation.
+The handler signature is the prototype that a callback function
+passed as completion token would need to have to be compatible with the operation.
+
+The handler signature for [refmemunq connection_pool async_get_connection]
+is `void(boost::system::error_code, mysql::pooled_connection)`.
+
+However, when we invoke `co_await` on the awaitable returned by `async_get_connection`,
+we don't get any `error_code`. This is because `co_await` inspects
+the handler signature at compile-time, looking for an `error_code` as first parameter.
+If it finds it, `co_await` will remove it from the argument list, returning
+only the `pooled_connection`. At runtime, the error code is checked.
+If the code indicates a failure, an exception is thrown.
+
+This mechanism is important to understand how `as_tuple` works.
+
+
+
+
+[heading asio::as_tuple]
+
+[asioreflink as_tuple as_tuple] is another adapter completion token
+that can be used to prevent exceptions. It modifies the operation's
+handler signature, packing all arguments into a `std::tuple`.
+This inhibits the automatic error code checks explained in the previous section,
+thus preventing exceptions on I/O failure. Continuing with our example:
+
+[tutorial_error_handling_as_tuple]
+
+In practice, it's usually better to use structured bindings:
+
+[tutorial_error_handling_as_tuple_structured_bindings]
+
+All the properties of adapter completion tokens apply:
+
+[tutorial_error_handling_as_tuple_default_tokens]
+
+Adapter tokens can be combined. To apply a timeout to the operation
+while avoiding exceptions, you can use:
+
+[tutorial_error_handling_as_tuple_cancel_after]
+
+
+
+
+
+[heading Using asio::as_tuple for database code]
+
+Let's apply [asioreflink as_tuple as_tuple] to our database logic.
+We will remove timeouts for now - we will add them back later.
+
+[tutorial_error_handling_db_nodiag]
+
+
+
+
+
+
+[heading Diagnostics objects]
+
+While what we wrote works, it can be improved. When a database operation fails, the server
+may supply an error string with information about what went wrong. Boost.MySQL may also
+generate such strings in certain cases. We get this automatically
+when using exceptions. Thanks to [reflink with_diagnostics] and default completion tokens,
+the library throws [reflink error_with_diagnostics] objects,
+which inherit from `boost::system::system_error`
+and have a [refmemunq error_with_diagnostics get_diagnostics] member.
+
+When using error codes, we need to handle diagnostics manually.
+All functions in Boost.MySQL are overloaded to accept a [reflink diagnostics]
+output parameter. It will be populated with extra information in case of error.
+
+Let's update our code to use diagnostics:
+
+[tutorial_error_handling_db]
+
+We also need to write the function to log errors:
+
+[tutorial_error_handling_log_error]
+
+[refmem diagnostics client_message] and [refmem diagnostics server_message] differ
+in their origin. Client messages never contain user-supplied input, and can always
+be used safely. Server messages may contain user input, and should be treated with
+more caution (logging them is fine).
+
+
+
+
+
+[heading Using asio::as_tuple with client reads and writes]
+
+Since `asio::read` and `asio::write` are compliant async operations,
+we can use [asioreflink as_tuple as_tuple] with them, too:
+
+[tutorial_error_handling_session_as_tuple]
+
+
+
+
+
+[heading Timeouts]
+
+Our session handler has three logical steps:
+
+* Read a request from the client.
+* Access the database.
+* Write the response to the client.
+
+Each of these steps may take long to complete. We will set a separate timeout
+to each one.
+
+Client reads and writes are the easiest ones to handle -
+we just need to combine `as_tuple` and `cancel_after`:
+
+[tutorial_error_handling_read_timeout]
+
+The database logic is more involved. Ideally, we would like
+to set a timeout to the overall database access operation, rather
+than to individual steps. However, a `co_await` expression
+isn't an async operation, and can't be passed a completion token.
+We can fix this by replacing plain `co_await` by `asio::co_spawn`,
+which accepts a completion token:
+
+[tutorial_error_handling_db_timeout]
+
+With these modifications, the session handler becomes:
+
+[tutorial_error_handling_session]
+
+With these modifications, our server is ready!
+
+
+
+[heading Wrapping up]
+
+Full program listing for this tutorial is [link mysql.examples.tutorial_error_handling here].
+
+This concludes our tutorial series. You can now look at the [link mysql.overview overview section]
+to learn more about the library features, or to the [link mysql.examples example section]
+if you prefer to learn by doing.
+
+[endsect]
\ No newline at end of file
diff --git a/doc/qbk/04_overview.qbk b/doc/qbk/04_overview.qbk
index c83a706ab..90f0742d7 100644
--- a/doc/qbk/04_overview.qbk
+++ b/doc/qbk/04_overview.qbk
@@ -213,9 +213,8 @@ that don't retrieve data:
When performing INSERTs, you might find [refmem results last_insert_id]
handy, which retrieves the last AUTO INCREMENT ID generated by the executed statement.
-You can run any SQL statement that MySQL supports, including
-`START TRANSACTION` and `COMMIT`.
-See [link mysql.examples.multi_queries_transactions this example] for more info.
+See [link mysql.tutorial_updates_transactions our tutorial on UPDATEs and transactions]
+for more info.
[endsect]
diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk
index 8436b8c58..2a83b754c 100644
--- a/doc/qbk/05_connection_establishment.qbk
+++ b/doc/qbk/05_connection_establishment.qbk
@@ -231,12 +231,9 @@ is disabled by default. You can enable it by setting
[section_connection_establishment_multi_queries]
-Semicolon-separated queries are useful in a number of cases, like when using transactions:
-
-[section_connection_establishment_multi_queries_execute]
-
-See the [link mysql.examples.multi_queries_transactions full example here].
-
+As explained [link mysql.tutorial_updates_transactions in the tutorial],
+multi-separated queries are useful in a number of cases,
+like when using transactions.
[link mysql.multi_resultset.multi_queries This section] contains more
info on how to use multi-queries.
diff --git a/doc/qbk/13_async.qbk b/doc/qbk/13_async.qbk
index f408e7d1a..a1efc01c7 100644
--- a/doc/qbk/13_async.qbk
+++ b/doc/qbk/13_async.qbk
@@ -108,8 +108,8 @@ is left in an unspecified state, after which you should close or destroy the con
In particular, it is [*not] safe to retry the cancelled operation.
Supporting cancellation allows you to implement timeouts without explicit
-support from the library. [link mysql.examples.timeouts This example]
-demonstrates how to implement this pattern.
+support from the library. [link mysql.tutorial_error_handling This tutorial]
+covers the subject in depth.
Note that cancellation happens at the Boost.Asio level, and not at the
MySQL operation level. This means that, when cancelling an operation, the
diff --git a/doc/qbk/17_charsets.qbk b/doc/qbk/17_charsets.qbk
index eca8ba49d..732407832 100644
--- a/doc/qbk/17_charsets.qbk
+++ b/doc/qbk/17_charsets.qbk
@@ -6,71 +6,222 @@
]
-[section:charsets Character sets]
+[section:charsets Character sets and collations]
[nochunk]
-[heading Character set refresher]
+According to [mysqllink charset.html MySQL docs], a [*character set] is
+['a set of symbols and their respective encodings].
+`utf8mb4`, `utf16` and `ascii` are character sets supported by MySQL.
+A [*collation] is a set of rules to compare characters, and is associated
+to a single character set. For example, `utf8mb4_spanish_ci` compares
+`utf8mb4` characters in a case-insensitive way.
-MySQL defines a character set as "a set of symbols and their respective encodings". `utf8mb4`,
-`utf16` and `ascii` are character sets supported by MySQL.
-A collation is a set of rules for comparing characters in a character set. For example, a case-insensitive
-collation will make strings that only differ in case compare equal. All collations are associated to a single
-character set. For example, `utf8mb4_spanish_ci` is a case-insensitive collation associated to the `utf8mb4` character
-set. Every character set has a default collation, which will be used if a character set without a collation is specified.
-For example, `latin1_swedish_ci` is the default collation for the `latin1` character set.
-You can find more information about these concepts in [mysqllink charset.html the official MySQL docs on character sets].
[heading The connection character set and collation]
-Every connection has an associated character set and collation. The connection's character set determines
-the encoding for character strings sent to and retrieved from the server. This includes SQL query strings,
-string fields and column names in metadata. The connection's collation is used for string literal comparison.
+Every client session has an associated character set and collation.
+The [*connection's character set determines the encoding for character strings
+sent to and retrieved from the server].
+This includes SQL query strings, string fields and column names in metadata.
+The connection's collation is used for string literal comparison.
+The connection's character set and collation can be changed dynamically
+using SQL.
-Every session you establish can have its own different
-character set and collation. You can specify this in two ways:
+By default, Boost.MySQL connections use `utf8mb4_general_ci`,
+thus [*using UTF-8 for all strings]. We recommend using this default,
+as MySQL character sets are easy to get wrong.
-* When calling [refmem any_connection async_connect], using
- [refmem connect_params connection_collation]. You specify a numeric ID that identifies
- the collation to use, and your connection will use the character set associated to this collation.
- You can find collation IDs in the [include_file boost/mysql/mysql_collations.hpp] and
- [include_file boost/mysql/mariadb_collations.hpp] headers.
+The connection's character set is not linked to the character set
+specified for databases, tables and columns.
+Consider the following declaration:
- The problem with this approach is that if you specify a collation ID that is unknown to the server
- (e.g. `utf8mb4_0900_ai_ci` for an old MySQL 5.7 server), the handshake operation
- will succeed but the connection [*will silently fall back to the server's default character set],
- (usually `latin1`, which is not Unicode).
-* At any time, using [refmem any_connection async_set_character_set].
+```
+CREATE TABLE test_table(
+ col1 TEXT CHARACTER SET utf16 COLLATE utf16_spanish_ci
+);
+```
+
+Data stored in `col1` will be encoded using UTF-16 and use
+`utf16_spanish_ci` for comparisons. However, when sent to
+the client, [*it will be converted to the connection's character set].
+
+[note
+ `utf8mb4` is how MySQL calls regular UTF-8. Confusingly,
+ MySQL has a character set named `utf8` which is not UTF-8 compliant.
+]
+
+
+
+
+
+[heading Connection character set effects]
+
+The connection's character set is crucial because it affects
+the encoding of most string fields. The following is a summary
+of what's affected:
+
+* SQL query strings passed to [refmemunq any_connection async_execute] and
+ [refmemunq any_connection async_prepare_statement] must be sent using
+ the connection's character set. Otherwise, server-side parsing errors may happen.
+* SQL templates and string values passed to [reflink with_params]
+ and [reflink format_sql] must be encoded using the connection's character set.
+ Otherwise, values will be rejected by Boost.MySQL when composing the query.
+ Connections [link mysql.charsets.tracking track the character set in use] to detect these errors.
+ If you bypass character set tracking (e.g. by using `SET NAMES` instead of
+ [refmemunq any_connection async_set_character_set]), you may run into vulnerabilities.
+* Statement string parameters passed to [refmem statement bind] should use the connection's character set.
+ Otherwise, MySQL may reject the values.
+* String values in rows and metadata retrieved from the server use the connection's character set.
+* Server-supplied diagnostic messages ([refmem diagnostics server_message]) also
+ use the connection's character set.
+
+To sum up, to properly use a connection, it's crucial to know
+the character set it's using.
+
+
+
+
+
+[heading Character set recommendations]
+
+The following sections provide a deep explanation on how character
+sets work in MySQL. If you don't have the time to read them,
+stick to the following advice:
+
+* [*Always use the default UTF-8]. Character sets in MySQL are complex and full of caveats.
+ If you need to use a different encoding in your application, convert your data to/from UTF-8
+ when interacting with the server. The default [reflink connect_params] ensure that UTF-8 is
+ used, without the need to run any SQL.
+* [*Don't execute SET NAMES] statements or change the `character_set_client` and
+ `character_set_results` session variables using `async_execute`.
+ This breaks character set tracking, which can lead to vulnerabilities.
+* Don't use [refmemunq any_connection async_reset_connection] unless you know what you're doing.
+ If you need to reuse connections, use [reflink connection_pool], instead.
+* Connections obtained from a [reflink connection_pool] always use `utf8mb4`.
+ When connections are returned to the pool, their character set is reset to `utf8mb4`.
+
+
+
+
+[heading:tracking Character set tracking]
+
+There is a number of actions that can change the connection's character set:
+
+* When connecting with [refmemunq any_connection async_connect],
+ a numeric collation ID is supplied to the server.
+ You can change it using [refmem connect_params connection_collation].
+ The [include_file boost/mysql/mysql_collations.hpp] and
+ [include_file boost/mysql/mariadb_collations.hpp] headers contain
+ available collation IDs.
+ If the server recognizes the passed collation, the connection's character set
+ will be the one associated to the collation. If it doesn't, the connection
+ [*will silently fall back to the server's default character set] (usually `latin1`, which is not Unicode).
+ This can happen when trying to use a newer collation, like `utf8mb4_0900_ai_ci`,
+ with an old MySQL 5.7 server. By default, Boost.MySQL uses
+ `utf8mb4_general_ci`, supported by all servers.
+* Using [refmemunq any_connection async_reset_connection] resets
+ the connection's character set [*to the server's default character set].
+* Using [refmemunq any_connection async_set_character_set] executes
+ a `SET NAMES` statement to set the connection's character set.
+ Executing a pipeline with a set character set stage has the same results.
+* Manually executing a `SET NAMES`, `SET CHARACTER SET` or modifying
+ the `character_set_client` and `character_set_results` change the
+ connection's character set. [*Don't do this], as it will confuse
+ character set tracking.
+
+[reflink any_connection] attempts to track the connection's current character set
+because it's required to securely perform client-side SQL formatting.
+This info is available using [refmem any_connection current_character_set],
+which returns a [reflink character_set] object.
+The current character set is also used by
+`async_execute` when a [reflink with_params_t] object is passed,
+and by [refmem any_connection format_opts].
+
+The MySQL protocol has limited support for character set tracking, so this task
+requires some help from the user. Some situations can make the current character set
+to be unknown. If this happens, executing a [reflink with_params_t] fails with
+`client_errc::unknown_character_set`. [refmem any_connection current_character_set]
+and [refmem any_connection format_opts] also return this error.
+
+Following the above points, this is how tracking works:
+
+* Before connection establishment, the current character set is always unknown.
+* After [refmemunq any_connection async_connect] succeeds,
+ conservative heuristics are used to determine the current character set.
+ If the passed [refmem connect_params connection_collation] is known to be
+ accepted by all supported servers, its associated character set becomes the
+ current one. If the library is not sure, the current character set is left unknown
+ (this is the safe choice to avoid vulnerabilities).
+ Note that leaving [refmemunq connect_params connection_collation] to its default value
+ always sets the current character set to [reflink utf8mb4_charset].
+* A successful [refmemunq any_connection async_set_character_set]
+ sets the current character set to the passed one.
+ The same applies for a successful set character set pipeline stage.
+* Calling [refmemunq any_connection async_reset_connection]
+ makes the current character set unknown.
[warning
- [*Do not use SET NAMES statements directly], as it will break
- [link mysql.charsets.tracking character set tracking], required
- for client-side SQL formatting.
+ [*Do not execute `SET NAMES`], `SET CHARACTER SET` or any other SQL statement
+ that modifies `character_set_client` using `async_execute`. This will make character set
+ information stored in the client invalid.
]
+
+
+
+[heading:custom Adding support for a character set]
+
+Built-in support is provided for `utf8mb4` ([reflink utf8mb4_charset])
+and `ascii` ([reflink ascii_charset]). We strongly encourage you to always use `utf8mb4`.
+Note that MySQL doesn't support setting the connection's character set
+to UTF-16 or UTF-32.
+
+If you really need to use a different character set, you can implement them by
+creating [reflink character_set] objects. You can then pass them to functions
+like [refmemunq any_connection set_character_set] like the built-in ones.
+
+[note
+ This is an advanced technique. Don't use it unless you know what you are doing.
+]
+
+The structure has the following members:
+
+* [refmem character_set name] must match the name you would use in `SET NAMES`.
+* [refmem character_set next_char] is used to iterate the string. It must return
+ the length in bytes of the first code point in the string, or 0 if the code point is invalid.
+
+For example, this is how you could implement the `utf8mb4` character set. For brevity, only
+a small part of the implementation is shown - have a look at the definition of [reflink utf8mb4_charset]
+for a full implementation.
+
+[charsets_next_char]
+
+
+
+
+
[heading character_set_results and character_set_client]
-Both of the above methods are shortcuts to set several session-level variables.
-The ones that impact this library's behavior are:
+Setting the connection's character set during connection establishment
+or using [refmemunq any_connection async_set_character_set] has the ultimate
+effect of changing some session variables. This section lists them as
+a reference. We [*strongly encourage you not to modify them manually],
+as this will confuse character set tracking.
* [mysqllink server-system-variables.html#sysvar_character_set_client character_set_client]
determines the encoding that SQL statements sent to the server should have. This includes
- the SQL strings passed to [refmem connection execute] and [refmem connection prepare_statement], and
+ the SQL strings passed to [refmemunq any_connection async_execute] and
+ [refmemunq any_connection async_prepare_statement], and
string parameters passed to [refmem statement bind].
-
- Not all character sets are permissible in `character_set_client`. The server will accept setting
- this variable to any UTF-8 character set, but won't accept UTF-16.
+ Not all character sets are permissible in `character_set_client`.
+ For example, UTF-16 and UTF-32 based character sets won't be accepted.
* [mysqllink server-system-variables.html#sysvar_character_set_results character_set_results]
determines the encoding that the server will use to send any kind of result, including
string fields retrieved by [refmem connection execute], metadata
like [refmem metadata column_name] and error messages.
-
- Note that, when you define a string column with a character set (e.g.
- `"CREATE TABLE t1 (col1 VARCHAR(5) CHARACTER SET latin1)"`), the column's character set
- will be used for storage and comparisons, but not for client communication. If you set `character_set_results`
- to `utf16`, any field obtained by `SELECT`ing `col1` will be UTF16-encoded, and not latin1-encoded.
- Note also that [refmem metadata column_collation] reflects the charset and collation the server
+ Note that [refmem metadata column_collation] reflects the character set and collation the server
has converted the column to before sending it to the client. In the above example, `metadata::column_collation`
will be the default collation for UTF16, rather than `latin1_swedish_ci`.
@@ -83,7 +234,16 @@ The table below summarizes the encoding used by each piece of functionality in t
[Encoding given by...]
]
[
- [SQL query strings passed to [refmem connection execute] and [refmem connection prepare_statement]]
+ [
+ SQL query strings passed to [refmemunq any_connection async_execute]
+ and [refmemunq any_connection async_prepare_statement]
+ ]
+ [`character_set_client`]
+ ]
+ [
+ [
+ Strings used with [reflink with_params] and [reflink format_sql]
+ ]
[`character_set_client`]
]
[
@@ -92,9 +252,7 @@ The table below summarizes the encoding used by each piece of functionality in t
]
[
[
- String fields retrieved by [refmem connection execute] or [refmem connection read_some_rows]:[br][br]
- [refmem field_view as_string][br]
- [refmem field_view get_string]
+ String fields in rows retrieved from the server
]
[`character_set_results`]
]
@@ -126,67 +284,9 @@ The table below summarizes the encoding used by each piece of functionality in t
]
]
-[heading:tracking Character set tracking]
-
-[reflink any_connection] attempts to track the connection's current character set.
-You can access this information using
-[refmem any_connection current_character_set] and [refmem any_connection format_opts].
-[note
- This functionality is only relevant when using SQL formatting and escaping functions,
- like [reflink format_sql], [reflink format_context] or [reflink escape_string].
-]
-The MySQL protocol has limited support for character set tracking, so this task
-requires some help from the user. Some situations can make the current character set
-to be unknown. If this happens, [refmem any_connection current_character_set]
-and [refmem any_connection format_opts] return an `unknown_character_set` error.
-This is how tracking works:
-
-* Before connection establishment, the current character set is always unknown.
-* After [refmem any_connection connect] or [refmemunq any_connection async_connect] succeed,
- heuristics are used to determine the current character set. This is required because the
- server may reject the collation requested by [refmem connect_params connection_collation]
- and silently fall back to an unknown character set. If Boost.MySQL is not sure that
- the collation will be accepted, the current character set will be left unknown.
- Note that leaving [refmem connect_params connection_collation] to its default value
- always sets the current character set to [reflink utf8mb4_charset].
-* After connection, you can call [refmem any_connection set_character_set] or
- [refmemunq any_connection async_set_character_set] to set the current character set to a known value.
- This will issue a `SET NAMES` statement and also update the value stored in the client.
-* Calling [refmem any_connection reset_connection] or [refmemunq any_connection async_reset_connection]
- resets the character set to the server's default, which is unknown (usually `latin1`). The current
- character set will be unknown until you call [refmemunq any_connection set_character_set] or
- [refmemunq any_connection async_set_character_set].
-
-[warning
- [*Do not execute `SET NAMES`], `SET CHARACTER SET` or any other SQL statement
- that modifies `character_set_client` using `execute`. This will make character set
- information stored in the client invalid.
-]
-
-
-[heading:custom Adding support for a character set]
-
-Built-in support is provided for `utf8mb4` ([reflink utf8mb4_charset])
-and `ascii` ([reflink ascii_charset]). We strongly encourage you to always use `utf8mb4`.
-
-If you really need to use a different character set, you can implement them by
-creating [reflink character_set] objects. You can then pass them to functions
-like [refmemunq any_connection set_character_set] like the built-in ones.
-
-The structure has the following members:
-
-* [refmem character_set name] must match the name you would use in `SET NAMES`.
-* [refmem character_set next_char] is used to iterate the string. It must return
- the length in bytes of the first code point in the string, or 0 if the code point is invalid.
-
-For example, this is how you could implement the `utf8mb4` character set. For brevity, only
-a small part of the implementation is shown - have a look at the definition of [reflink utf8mb4_charset]
-for a full implementation.
-
-[charsets_next_char]
[endsect]
diff --git a/doc/qbk/19_templated_connection.qbk b/doc/qbk/19_templated_connection.qbk
index 39d470e8a..2f2badb86 100644
--- a/doc/qbk/19_templated_connection.qbk
+++ b/doc/qbk/19_templated_connection.qbk
@@ -23,8 +23,7 @@ level of efficiency.
[heading Streams and type aliases]
[reflink connection] is templated on the [reflink Stream] class,
-which implements the transport layer to read and write bytes
-from the wire.
+which implements the transport layer to read and write wire bytes.
The library provides helper type aliases for the most common cases:
@@ -64,6 +63,8 @@ The same three transports above can be used with `any_connection`.
+
+
[heading Constructing a connection]
`connection`'s constructor takes the same arguments as the underlying `Stream` constructor.
@@ -76,154 +77,261 @@ a __ssl_context__:
-[heading Connection establishment and termination]
+[heading Connection establishment]
+
+Use [refmem connection connect] or [refmem connection async_connect] to perform connection
+establishment. This function takes two parameters:
-When using TCP,
+* An endpoint to connect to. The endpoint type depends on the stream type.
+ For TCP connections, it's an [asioreflink ip__tcp/endpoint asio::ip::tcp::endpoint],
+ which holds an IP address and a port. For UNIX sockets, it'd be an
+ [asioreflink local__stream_protocol/endpoint asio::local::stream_protocol::endpoint],
+ holding a UNIX path.
+* A [reflink handshake_params] instance, containing all the parameters required
+ to perform the MySQL handshake.
-* connection does not know about name resolution. You need
- to perform this yourself.
-* Parameters are passed as two arguments to connect (see example)
-* handshake_params is used instead of connect_params.
- handshake_params is non-owning and doesn't include the server address.
-* connection exposes handshake and quit. Using these, you can perform
- transport-level connection establishment yourself, and then call
- handshake. Same for quit. These are no longer exposed in any_connection,
- since not exposing them allows for stronger guarantees.
-* Once a connection closes or suffers a cancellation, tcp_ssl_connection can't
- be re-connected. It needs to be destroyed and created again.
- any_connection::connect can always be called to re-connect a connection,
- no matter what happened.
-* Explain SocketStream and Stream
+If you're using TCP, you must perform hostname resolution yourself.
+For example:
-TODO: check that evth down here is above too
-[reflink any_connection] is a type-erased alternative to [reflink connection].
-It's easier to use and features more functionality than plain `connection`.
+[templated_connection_connect]
-When compared to [reflink connection], `any_connection`:
+As opposed to `connect_params`, [reflink handshake_params] does not own
+the strings it contains (like the username and the password). It's your responsibility
+to keep them alive until the connect operation completes.
+
+All functionality in [reflink handshake_params] has an equivalent in
+[reflink connect_params]. See the [link mysql.templated_connection.reference reference table]
+for more info.
-* Is type-erased. The type of the connection doesn't depend on the transport being used.
- Supported transports include plaintext TCP, TLS on top of TCP and UNIX domain sockets.
-* Is easier to connect. For example, when using TCP, connection establishment methods will
- handle hostname resolution for you. This must be handled manually with `connection`.
-* Can always be reconnected after closing it or after encountering an error.
- `connection` can't make this guarantee, especially when using TLS.
-* Doesn't allow to customize the internal `Stream` type. Doing this
- allows supporting the point above.
-* Has `with_diagnostics(asio::deferred)` as default completion token,
- which allows using `co_await` and getting exceptions with extra information.
-* Has equivalent performance.
-* Other than session establishment, it has the same API as `connection`.
-`any_connection` is expected to replace `connection` in the long run.
[heading Using a connection]
-Other than that, connection and any_connection are used almost equivalently.
-They support the same APIs, like execute, prepare_statement, close_statement
-and reset_connection.
+Once connected, [reflink connection] and [reflink any_connection] can
+be used almost equivalently:
-Some newer APIs only present in any_connection, like set_character_set or pipeline.
-any_connection's default completion token is with_diagnostics(asio::deferred),
-allowing easier interoperability with coroutines.
+[templated_connection_use]
+Some differences:
+* Some newer APIs, like [refmemunq any_connection async_set_character_set]
+ and [refmemunq any_connection async_run_pipeline], are not present
+ in [reflink connection].
+* By default, `connection`'s completion token is `asio::deferred`
+ instead of `mysql::with_diagnostics(asio::deferred)`. When using
+ coroutines with exceptions, you need to pass `mysql::with_diagnostics`
+ explicitly if you want exceptions with extra info.
-[heading Migrating to any_connection]
-If you're using tcp_connection, tcp_ssl_connection or unix_connection,
-we strongly recommend migrating to any_connection. You need to update
-your `connect`, to use `connect_params` (which probably simplifies your code).
-TODO: place this somewhere
-[heading SSL-enabled streams]
-To use SSL/TLS, you must use a [reflink connection] with a
-[reflink Stream] that supports SSL. A SSL-enabled stream must inherit from
-[asioreflink ssl__stream_base ssl::stream_base]. This includes both
-[asioreflink ssl__stream ssl::stream] and `boost::beast::ssl_stream`.
-To make life easier, this library provides the type alias [reflink tcp_ssl_connection].
+[heading Terminating a connection]
-Note that there is no need to use TLS when using UNIX sockets. As the traffic doesn't
-leave the machine, MySQL considers them secure, and will allow using authentication
-plugins like `caching_sha2_password` even if TLS is not used.
+As with `any_connection`, use [refmem connection close] or [refmemunq connection async_close]:
-[heading:non_sockets Streams that are not sockets]
-When the `Stream` template argument for your `connection` fulfills
-the __SocketStream__ type requirements, you can use the member functions
-[refmem connection connect] and [refmem connection close] to establish and finish
-connections with the MySQL server. If you are using any of the convenience type
-aliases (TCP or UNIX, either over TLS or not), then this is your case.
-If your stream type is not based on a socket, you can't use those convenience member
-functions. This would be the case if you are using Windows named pipes
-(i.e. [asioreflink windows__stream_handle windows::stream_handle]).
-Instead, to establish a connection, you should follow these two steps,
-roughly equivalent to what [refmem connection connect] does for sockets:
-* Connect the underlying stream. You can access it using
- [refmem connection stream]. Use whatever connection establishment
- mechanism the stream implements. If you are using TLS, you should *not*
- perform the TLS handshake yourself, as the library will do it as part of the
- MySQL handshake.
-* Perform the MySQL handshake by calling [refmem connection handshake]
- or [refmem connection async_handshake]. If the handshake operation
- fails, close the stream.
-
-To clean up a connection, follow these two steps,
-roughly equivalent to [refmem connection close]:
+[heading TLS support]
-* Inform the MySQL server that you are quitting the connection
- by calling [refmem connection quit] or [refmem connection async_quit].
- This will also shutdown TLS, if it's being used.
-* Close the underlying stream.
+To use TLS, you must use a [reflink connection] with a
+[reflink Stream] that supports TLS.
+A ['TLS-enabled stream] must inherit from
+[asioreflink ssl__stream_base ssl::stream_base].
+The most common is
+[asioreflink ssl__stream ssl::stream] (used by [reflink tcp_ssl_connection]).
+When using a stream type that does not support TLS, like [reflink tcp_connection]
+or [reflink unix_connection], [refmem handshake_params ssl] is ignored.
-[heading Reconnecting]
-TODO: review this
-After you close a connection or an error has occurred, and if its underlying [reflink Stream]
-supports it, you can re-open an existing connection. This is the case for
-[reflink tcp_connection] and [reflink unix_connection].
+[heading UNIX sockets]
+
+
+To use UNIX sockets, use [reflink unix_connection]:
+
+[templated_connection_unix]
+
+
+
+
+[heading Handshake and quit]
+
+In addition to [refmemunq connection connect] and [refmemunq connection close],
+`connection` exposes two additional I/O operations:
+
+* [refmem connection handshake] is like `connect`, but doesn't connect
+ the underlying `Stream`.
+* [refmem connection quit] is like `close`, but doesn't close
+ the underlying `Stream`.
+
+You can use them like this:
+
+[templated_connection_handshake_quit]
+
+These functions can be useful in the following cases:
+
+* When you want to perform stream connection establishment yourself.
+ For example, when you want to use the range `asio::connect`
+ overloads, as in the example above.
+* When using an exotic `Stream` type. `connect` and `close`
+ can only be used if the `Stream` type satisfies [reflink SocketStream] -
+ that is, when its lowest layer type is a socket. This holds for
+ all the stream types in the table above, but is not the case
+ for [asioreflink windows__stream_handle windows::stream_handle].
+
+
-[warning
- Unfortunately, [asioreflink ssl__stream ssl::stream] does not support reconnection.
- If you are using [reflink tcp_ssl_connection] and you close
- the connection or encounter an error, you will have to destroy and re-create the connection object.
-]
-If you are using [reflink tcp_connection] or [reflink unix_connection], or any other stream supporting
-reconnection:
+[heading Reconnection]
-* After calling [refmem connection close], you can re-open the connection later by calling
- [refmem connection connect] normally, even if the close operation failed.
+The reconnection capabilities of `connection` are more limited than those of `any_connection`.
+Concretely, when using TLS-capable streams, a `connection` can't be re-used after
+it's closed or encounters a fatal error. This is because [asioreflink ssl__stream ssl::stream]
+can't be re-used. This limitation is not present in [reflink any_connection].
+
+If you are using [reflink tcp_connection] or [reflink unix_connection],
+or any other stream supporting reconnection, and you want to re-use a connection:
+
+* Call [refmem connection close], or manually close the underlying stream,
+ even if you encountered a fatal error.
+* Call [refmem connection connect] normally, even if the close operation failed.
* If your [refmem connection connect] operation failed, you can try opening it again
by simply calling [refmem connection connect] again.
-* If you connected your connection successfully but encountered a network problem in any subsequent operation,
- and you would like to re-establish connection, you should first call [refmem connection close] first, and
- then try opening the connection again by calling [refmem connection connect].
-
-If your `Stream` type doesn't fulfill the __SocketStream__ type requirements,
-then you can't use [refmem connection connect] or [refmem connection close], and you are thus
-responsible for establishing the physical connection
-and closing the underlying stream, if necessary. Some guidelines:
-
-* After calling [refmem connection quit], you should close the underlying stream, if required.
- You should then re-establish the physical connection on the stream, and call [refmem connection handshake] afterwards.
-* If your [refmem connection handshake] operation failed, you are responsible for closing the underlying stream if required.
- You should then establish the physical connection again, and then call [refmem connection handshake].
-* If you connected your connection successfully but encountered a network problem in any subsequent operation,
- and you would like to re-establish connection, you should call [refmem connection quit] first, then close and re-open
- the physical connection, and finally call [refmem connection handshake].
-
-Note that __Self__ does not perform any built-in retry strategy, as different use cases have different requirements.
-You can implement it as you best like with these tools. If you implemented your own and you would like to contribute it,
-please create a PR in the GitHub repository.
+If your `Stream` type doesn't fulfill the [reflink SocketStream] concept,
+you need to use [refmemunq connection handshake] and [refmemunq connection quit]
+instead of `connect` and `close`, and perform transport connection establishment yourself.
+
+As with `any_connection`, `connection` doesn't perform any built-in retry strategy.
+
+
+
+
+
+[heading Migrating to any_connection]
+
+We recommend migrating code using templated connections to `any_connection`.
+In most cases, you only need to change connection establishment code
+to use [reflink connect_params] instead of [reflink handshake_params].
+
+The following table summarizes all the differences between the
+two connection types, and provides migration paths for each feature
+you may use:
+
+[table:reference
+ [
+ [Feature]
+ [any_connection]
+ [connection]
+ ]
+ [
+ [Hostname resolution]
+ [Performed by [refmem any_connection async_connect]]
+ [Needs to be performed manually]
+ ]
+ [
+ [Credentials]
+ [
+ [refmem connect_params username], [refmem connect_params password]
+ ]
+ [
+ [refmem handshake_params username], [refmem handshake_params password]
+ ]
+ ]
+ [
+ [Database to use]
+ [[refmem connect_params database]]
+ [[refmem handshake_params database]]
+ ]
+ [
+ [Setting TLS options]
+ [
+ [refmem any_connection_params ssl_context]
+ ]
+ [
+ Pass a __ssl_context__ to [reflink tcp_ssl_connection]'s constructor.
+ ]
+ ]
+ [
+ [TLS negotiation]
+ [[refmem connect_params ssl]. Ignored for if using UNIX sockets. Defaults to `mysql::ssl_mode::enable`.]
+ [[refmem handshake_params ssl]. Ignored if `Stream` is not TLS-enabled. Defaults to `mysql::ssl_mode::require`.]
+ ]
+ [
+ [Connection collation]
+ [[refmem connect_params connection_collation]]
+ [[refmem handshake_params connection_collation]]
+ ]
+ [
+ [Enabling multi-queries]
+ [[refmem connect_params multi_queries]]
+ [[refmem handshake_params multi_queries]]
+ ]
+ [
+ [UNIX sockets]
+ [Use a UNIX socket path in [refmem connect_params server_address]]
+ [Use [reflink unix_connection] and pass a UNIX endpoint to [refmem connection connect]]
+ ]
+ [
+ [Windows named pipes]
+ [Not available yet]
+ [Use [asioreflink windows__stream_handle windows::stream_handle] as stream type]
+ ]
+ [
+ [Changing the initial size of the internal network buffer]
+ [[refmem any_connection_params initial_buffer_size]]
+ [Pass a [reflink buffer_params] instance to connection's constructor]
+ ]
+ [
+ [Changing the network buffer size limit]
+ [[refmem any_connection_params max_buffer_size]]
+ [Not available: no limit on the network buffer size]
+ ]
+ [
+ [Access the underlying stream]
+ [Unavailable]
+ [[refmem connection stream]]
+ ]
+ [
+ [Raw handshake and quit]
+ [Unavailable]
+ [[refmem connection handshake], [refmem connection quit]]
+ ]
+ [
+ [Reconnection]
+ [[refmem any_connection async_connect] can always be used]
+ [
+ Requires closing the current connection first.
+ Unavailable for [reflink tcp_ssl_connection].
+ ]
+ ]
+ [
+ [Changing the connection's character set]
+ [[refmem any_connection async_set_character_set]]
+ [Unavailable]
+ ]
+ [
+ [Running pipelines]
+ [[refmem any_connection async_run_pipeline]]
+ [Unavailable]
+ ]
+ [
+ [Including diagnostics in coroutine exceptions]
+ [Enabled by default]
+ [[templated_connection_with_diagnostics]]
+ ]
+ [
+ [Connection pooling]
+ [[reflink connection_pool]]
+ [Unavailable]
+ ]
+]
+
[endsect]
diff --git a/doc/qbk/21_examples.qbk b/doc/qbk/21_examples.qbk
index de03d4bc0..c4246be0d 100644
--- a/doc/qbk/21_examples.qbk
+++ b/doc/qbk/21_examples.qbk
@@ -21,14 +21,17 @@ Self-contained programs demonstrating the basic concepts.
* [link mysql.examples.tutorial_async Tutorial 2 listing: going async with C++20 coroutines]
* [link mysql.examples.tutorial_with_params Tutorial 3 listing: queries with parameters]
* [link mysql.examples.tutorial_static_interface Tutorial 4 listing: the static interface]
+* [link mysql.examples.tutorial_updates_transactions Tutorial 5 listing: UPDATEs, transactions and multi-queries]
+* [link mysql.examples.tutorial_connection_pool Tutorial 6 listing: connection pools]
+* [link mysql.examples.tutorial_error_handling Tutorial 7 listing: error handling]
[heading Simple programs]
Self-contained programs demonstrating more advanced concepts and techniques.
+* [link mysql.examples.inserts INSERTs, last_insert_id() and NULL values]
+* [link mysql.examples.deletes DELETEs and affected_rows()]
* [link mysql.examples.prepared_statements Prepared statements]
-* [link mysql.examples.timeouts Setting timeouts to operations]
-* [link mysql.examples.multi_queries_transactions Using multi-queries and transactions]
* [link mysql.examples.disable_tls Disabling TLS for a connection]
* [link mysql.examples.tls_certificate_verification Setting TLS options: enabling TLS certificate verification]
* [link mysql.examples.metadata Metadata]
@@ -119,33 +122,66 @@ This example assumes you have gone through the [link mysql.examples.setup setup]
-[section:prepared_statements Prepared statements]
+[section:tutorial_updates_transactions Tutorial 5 listing: UPDATEs, transactions and multi-queries]
This example assumes you have gone through the [link mysql.examples.setup setup].
-[example_prepared_statements]
+[example_tutorial_updates_transactions]
+
+[endsect]
+
+
+
+
+[section:tutorial_connection_pool Tutorial 6 listing: connection pools]
+
+This example assumes you have gone through the [link mysql.examples.setup setup].
+
+[example_tutorial_connection_pool]
[endsect]
-[section:timeouts Setting timeouts to operations]
+[section:tutorial_error_handling Tutorial 7 listing: error handling]
This example assumes you have gone through the [link mysql.examples.setup setup].
-[example_timeouts]
+[example_tutorial_error_handling]
[endsect]
-[section:multi_queries_transactions Using multi-queries and transactions]
+[section:inserts INSERTs, last_insert_id() and NULL values]
This example assumes you have gone through the [link mysql.examples.setup setup].
-[example_multi_queries_transactions]
+[example_inserts]
+
+[endsect]
+
+
+
+
+[section:deletes DELETEs and affected_rows()]
+
+This example assumes you have gone through the [link mysql.examples.setup setup].
+
+[example_deletes]
+
+[endsect]
+
+
+
+
+[section:prepared_statements Prepared statements]
+
+This example assumes you have gone through the [link mysql.examples.setup setup].
+
+[example_prepared_statements]
[endsect]
diff --git a/doc/qbk/helpers/quickref.xml b/doc/qbk/helpers/quickref.xml
index 791a5d29d..7c345b873 100644
--- a/doc/qbk/helpers/quickref.xml
+++ b/doc/qbk/helpers/quickref.xml
@@ -148,7 +148,6 @@
WritableField typesFormattable typesPipeline stage reference (experimental)
- String encoding
diff --git a/example/1_tutorial/4_static_interface.cpp b/example/1_tutorial/4_static_interface.cpp
index 6308ee8cc..bc6d3bafb 100644
--- a/example/1_tutorial/4_static_interface.cpp
+++ b/example/1_tutorial/4_static_interface.cpp
@@ -18,6 +18,9 @@
* Like the previous tutorial, given an employee ID,
* it prints their full name.
*
+ * It uses Boost.Pfr for reflection, which requires C++20.
+ * You can backport it to C++14 if you need by using Boost.Describe.
+ *
* This example uses the 'boost_mysql_examples' database, which you
* can get by running db_setup.sql.
*/
diff --git a/example/2_simple/multi_queries_transactions.cpp b/example/1_tutorial/5_updates_transactions.cpp
similarity index 70%
rename from example/2_simple/multi_queries_transactions.cpp
rename to example/1_tutorial/5_updates_transactions.cpp
index 839f79c0d..fdc9db878 100644
--- a/example/2_simple/multi_queries_transactions.cpp
+++ b/example/1_tutorial/5_updates_transactions.cpp
@@ -5,33 +5,33 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
+#include
+
#include
-#ifdef BOOST_ASIO_HAS_CO_AWAIT
+#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
-//[example_multi_queries_transactions
+//[example_tutorial_updates_transactions
/**
- * This example demonstrates how to use multi-queries
- * to run several semicolon-separated queries in
- * a single async_execute call. It also demonstrates
- * how to use SQL transactions.
+ * This example demonstrates how to use UPDATE statements,
+ * transactions and semicolon-separated queries.
*
- * The program updates the first name of an employee,
- * and prints the employee's full details.
+ * The program updates the first name of an employee given their ID
+ * and prints their full details.
*
- * It uses C++20 coroutines. If you need, you can backport
- * it to C++11 by using callbacks, asio::yield_context
- * or sync functions instead of coroutines.
+ * It uses Boost.Pfr for reflection, which requires C++20.
+ * You can backport it to C++14 if you need by using Boost.Describe.
*
* This example uses the 'boost_mysql_examples' database, which you
* can get by running db_setup.sql.
*/
-
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
@@ -42,10 +42,20 @@
#include
#include
#include
+#include
namespace mysql = boost::mysql;
namespace asio = boost::asio;
+// As in the previous tutorial, this struct models
+// the data returned by our SELECT query. It should contain a member
+// for each field of interest, with a matching name.
+struct employee
+{
+ std::string first_name;
+ std::string last_name;
+};
+
// The main coroutine
asio::awaitable coro_main(
std::string_view server_hostname,
@@ -60,6 +70,7 @@ asio::awaitable coro_main(
mysql::any_connection conn(co_await asio::this_coro::executor);
//[section_connection_establishment_multi_queries
+ //[tutorial_updates_transactions_connect
// The server host, username, password and database to use.
// Setting multi_queries to true makes it possible to run several
// semicolon-separated queries with async_execute.
@@ -73,46 +84,59 @@ asio::awaitable coro_main(
// Connect to the server
co_await conn.async_connect(params);
//]
+ //]
// Perform the update and retrieve the results:
// 1. Begin a transaction block. Further updates won't be visible to
// other transactions until this one commits.
// 2. Perform the update.
// 3. Retrieve the employee we just updated. Since we're in a transaction,
- // the employee record will be locked at this point. This ensures that
- // we retrieve the employee we updated, and not an employee created
- // by another transaction. That is, this prevents dirty reads.
+ // this will be the employee we just updated (if any),
+ // without the possibility of other transactions interfering.
// 4. Commit the transaction and make everything visible to other transactions.
// If any of the previous steps fail, the commit won't be run, and the
// transaction will be rolled back when the connection is closed.
- mysql::results result;
+ //[tutorial_updates_transactions_static
+ // MySQL returns one resultset for each query, so we pass 4 params to static_results
+ //<-
+ // clang-format off
+ //->
+ mysql::static_results<
+ std::tuple<>, // START TRANSACTION doesn't generate rows
+ std::tuple<>, // The UPDATE doesn't generate rows
+ mysql::pfr_by_name, // The SELECT generates employees
+ std::tuple<> // The COMMIT doesn't generate rows
+ > result;
+ //<-
+ // clang-format on
+ //->
+
co_await conn.async_execute(
mysql::with_params(
"START TRANSACTION;"
- "UPDATE employee SET first_name = {1} WHERE id = {0};"
- "SELECT first_name, last_name FROM employee WHERE id = {0};"
+ "UPDATE employee SET first_name = {0} WHERE id = {1};"
+ "SELECT first_name, last_name FROM employee WHERE id = {1};"
"COMMIT",
- employee_id,
- new_first_name
+ new_first_name,
+ employee_id
),
result
);
// We've run 4 SQL queries, so MySQL has returned us 4 resultsets.
- // The SELECT is the 3rd resultset. Retrieve it
- mysql::resultset_view select_result = result.at(2);
-
- // resultset_view has a similar interface to results.
- // Retrieve the generated rows
- if (select_result.rows().empty())
+ // The SELECT is the 3rd resultset. Retrieve the generated rows.
+ // employees is a span
+ auto employees = result.rows<2>();
+ if (employees.empty())
{
std::cout << "No employee with ID = " << employee_id << std::endl;
}
else
{
- mysql::row_view employee = select_result.rows().at(0);
- std::cout << "Updated: employee is now " << employee.at(0) << " " << employee.at(1) << std::endl;
+ const employee& emp = employees[0];
+ std::cout << "Updated: employee is now " << emp.first_name << " " << emp.last_name << std::endl;
}
+ //]
// Notify the MySQL server we want to quit, then close the underlying connection.
co_await conn.async_close();
diff --git a/example/1_tutorial/6_connection_pool.cpp b/example/1_tutorial/6_connection_pool.cpp
new file mode 100644
index 000000000..11fb1e883
--- /dev/null
+++ b/example/1_tutorial/6_connection_pool.cpp
@@ -0,0 +1,305 @@
+//
+// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+
+#include
+
+#include
+#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
+
+//[example_tutorial_connection_pool
+
+/**
+ * This example demonstrates how to use connection_pool
+ * to implement a server for a simple custom TCP-based protocol.
+ * It also demonstrates how to set timeouts with asio::cancel_after.
+ *
+ * The protocol can be used to retrieve the full name of an
+ * employee, given their ID. It works as follows:
+ * - The client connects.
+ * - The client sends the employee ID, as a big-endian 64-bit signed int.
+ * - The server responds with a string containing the employee full name.
+ * - The connection is closed.
+ *
+ * This tutorial doesn't include proper error handling.
+ * We will build it in the next one.
+ *
+ * It uses Boost.Pfr for reflection, which requires C++20.
+ * You can backport it to C++14 if you need by using Boost.Describe.
+ * It uses C++20 coroutines. If you need, you can backport
+ * it to C++11 by using callbacks, asio::yield_context
+ * or sync functions instead of coroutines.
+ *
+ * This example uses the 'boost_mysql_examples' database, which you
+ * can get by running db_setup.sql.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace mysql = boost::mysql;
+namespace asio = boost::asio;
+
+// Should contain a member for each field of interest present in our query
+struct employee
+{
+ std::string first_name;
+ std::string last_name;
+};
+
+//[tutorial_connection_pool_db
+// Encapsulates the database access logic.
+// Given an employee_id, retrieves the employee details to be sent to the client.
+asio::awaitable get_employee_details(mysql::connection_pool& pool, std::int64_t employee_id)
+{
+ //[tutorial_connection_pool_get_connection_timeout
+ // Get a connection from the pool.
+ // This will wait until a healthy connection is ready to be used.
+ // pooled_connection grants us exclusive access to the connection until
+ // the object is destroyed.
+ // Fail the operation if no connection becomes available in the next 20 seconds.
+ mysql::pooled_connection conn = co_await pool.async_get_connection(
+ asio::cancel_after(std::chrono::seconds(1))
+ );
+ //]
+
+ //[tutorial_connection_pool_use
+ // Use the connection normally to query the database.
+ // operator-> returns a reference to an any_connection,
+ // so we can apply all what we learnt in previous tutorials
+ mysql::static_results> result;
+ co_await conn->async_execute(
+ mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id),
+ result
+ );
+ //]
+
+ // Compose the message to be sent back to the client
+ if (result.rows().empty())
+ {
+ co_return "NOT_FOUND";
+ }
+ else
+ {
+ const auto& emp = result.rows()[0];
+ co_return emp.first_name + ' ' + emp.last_name;
+ }
+
+ // When the pooled_connection is destroyed, the connection is returned
+ // to the pool, so it can be re-used.
+}
+//]
+
+//[tutorial_connection_pool_session
+asio::awaitable handle_session(mysql::connection_pool& pool, asio::ip::tcp::socket client_socket)
+{
+ // Read the request from the client.
+ // async_read ensures that the 8-byte buffer is filled, handling partial reads.
+ unsigned char message[8]{};
+ co_await asio::async_read(client_socket, asio::buffer(message));
+
+ // Parse the 64-bit big-endian int into a native int64_t
+ std::int64_t employee_id = boost::endian::load_big_s64(message);
+
+ // Invoke the database handling logic
+ std::string response = co_await get_employee_details(pool, employee_id);
+
+ // Write the response back to the client.
+ // async_write ensures that the entire message is written, handling partial writes
+ co_await asio::async_write(client_socket, asio::buffer(response));
+
+ // The socket's destructor will close the client connection
+}
+//]
+
+//[tutorial_connection_pool_listener
+asio::awaitable listener(mysql::connection_pool& pool, unsigned short port)
+{
+ // An object that accepts incoming TCP connections.
+ asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor);
+
+ // The endpoint where the server will listen.
+ asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port);
+
+ // Open the acceptor
+ acc.open(listening_endpoint.protocol());
+
+ // Allow reusing the local address, so we can restart our server
+ // without encountering errors in bind
+ acc.set_option(asio::socket_base::reuse_address(true));
+
+ // Bind to the local address
+ acc.bind(listening_endpoint);
+
+ // Start listening for connections
+ acc.listen();
+ std::cout << "Server listening at " << acc.local_endpoint() << std::endl;
+
+ // Start the accept loop
+ while (true)
+ {
+ // Accept a new connection
+ auto sock = co_await acc.async_accept();
+
+ // Function implementing our session logic.
+ // Take ownership of the socket.
+ // Having this as a named variable workarounds a gcc bug
+ // (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288)
+ auto session_logic = [&pool, s = std::move(sock)]() mutable {
+ return handle_session(pool, std::move(s));
+ };
+
+ // Launch a coroutine that runs our session logic.
+ // We don't co_await this coroutine so we can listen
+ // to new connections while the session is running.
+ asio::co_spawn(
+ // Use the same executor as the current coroutine
+ co_await asio::this_coro::executor,
+
+ // Session logic
+ std::move(session_logic),
+
+ // Propagate exceptions thrown in handle_session
+ [](std::exception_ptr ex) {
+ if (ex)
+ std::rethrow_exception(ex);
+ }
+ );
+ }
+}
+//]
+
+void main_impl(int argc, char** argv)
+{
+ if (argc != 5)
+ {
+ std::cerr << "Usage: " << argv[0] << " \n";
+ exit(1);
+ }
+
+ const char* username = argv[1];
+ const char* password = argv[2];
+ const char* server_hostname = argv[3];
+ auto listener_port = static_cast(std::stoi(argv[4]));
+
+ //[tutorial_connection_pool_main
+ //[tutorial_connection_pool_create
+ // Create an I/O context, required by all I/O objects
+ asio::io_context ctx;
+
+ // pool_params contains configuration for the pool.
+ // You must specify enough information to establish a connection,
+ // including the server address and credentials.
+ // You can configure a lot of other things, like pool limits
+ mysql::pool_params params;
+ params.server_address.emplace_host_and_port(server_hostname);
+ params.username = username;
+ params.password = password;
+ params.database = "boost_mysql_examples";
+
+ // Construct the pool.
+ // ctx will be used to create the connections and other I/O objects
+ mysql::connection_pool pool(ctx, std::move(params));
+ //]
+
+ //[tutorial_connection_pool_run
+ // You need to call async_run on the pool before doing anything useful with it.
+ // async_run creates connections and keeps them healthy. It must be called
+ // only once per pool.
+ // The detached completion token means that we don't want to be notified when
+ // the operation ends. It's similar to a no-op callback.
+ pool.async_run(asio::detached);
+ //]
+
+ //[tutorial_connection_pool_signals
+ // signal_set is an I/O object that allows waiting for signals
+ asio::signal_set signals(ctx, SIGINT, SIGTERM);
+
+ // Wait for signals
+ signals.async_wait([&](boost::system::error_code, int) {
+ // Stop the execution context. This will cause io_context::run to return
+ ctx.stop();
+ });
+ //]
+
+ // Launch our listener
+ asio::co_spawn(
+ ctx,
+ [&pool, listener_port] { return listener(pool, listener_port); },
+ // If any exception is thrown in the coroutine body, rethrow it.
+ [](std::exception_ptr ptr) {
+ if (ptr)
+ {
+ std::rethrow_exception(ptr);
+ }
+ }
+ );
+
+ // Calling run will actually execute the coroutine until completion
+ ctx.run();
+ //]
+}
+
+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 character set
+ // (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;
+ }
+}
+
+//]
+
+#else
+
+#include
+
+int main()
+{
+ std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example"
+ << std::endl;
+}
+
+#endif
diff --git a/example/1_tutorial/7_error_handling.cpp b/example/1_tutorial/7_error_handling.cpp
new file mode 100644
index 000000000..728f96d37
--- /dev/null
+++ b/example/1_tutorial/7_error_handling.cpp
@@ -0,0 +1,369 @@
+//
+// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+
+#include
+
+#include
+#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
+
+//[example_tutorial_error_handling
+
+/**
+ * This tutorial adds error handling to the program in the previous tutorial.
+ * It shows how to avoid exceptions and use diagnostics objects.
+ *
+ * It uses Boost.Pfr for reflection, which requires C++20.
+ * You can backport it to C++14 if you need by using Boost.Describe.
+ * It uses C++20 coroutines. If you need, you can backport
+ * it to C++11 by using callbacks, asio::yield_context
+ * or sync functions instead of coroutines.
+ *
+ * This example uses the 'boost_mysql_examples' database, which you
+ * can get by running db_setup.sql.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace mysql = boost::mysql;
+namespace asio = boost::asio;
+
+//[tutorial_error_handling_log_error
+// Log an error to std::cerr
+void log_error(const char* header, boost::system::error_code ec, const mysql::diagnostics& diag = {})
+{
+ // Inserting the error code only prints the number and category. Add the message, too.
+ std::cerr << header << ": " << ec << " " << ec.message();
+
+ // client_message() contains client-side generated messages that don't
+ // contain user-input. This is usually embedded in exceptions.
+ // When working with error codes, we need to log it explicitly
+ if (!diag.client_message().empty())
+ {
+ std::cerr << ": " << diag.client_message();
+ }
+
+ // server_message() contains server-side messages, and thus may
+ // contain user-supplied input. Printing it is safe.
+ if (!diag.server_message().empty())
+ {
+ std::cerr << ": " << diag.server_message();
+ }
+
+ // Done
+ std::cerr << std::endl;
+}
+//]
+
+// Should contain a member for each field of interest present in our query
+struct employee
+{
+ std::string first_name;
+ std::string last_name;
+};
+
+// Encapsulates the database access logic.
+// Given an employee_id, retrieves the employee details to be sent to the client.
+//[tutorial_error_handling_db
+asio::awaitable get_employee_details(mysql::connection_pool& pool, std::int64_t employee_id)
+{
+ // Will be populated with error information in case of error
+ mysql::diagnostics diag;
+
+ // Get a connection from the pool.
+ // This will wait until a healthy connection is ready to be used.
+ // ec is an error_code, conn is the mysql::pooled_connection
+ auto [ec, conn] = co_await pool.async_get_connection(diag, asio::as_tuple);
+ if (ec)
+ {
+ // A connection couldn't be obtained.
+ // This may be because a timeout happened.
+ log_error("Error in async_get_connection", ec, diag);
+ co_return "ERROR";
+ }
+
+ // Use the connection normally to query the database.
+ mysql::static_results> result;
+ auto [ec2] = co_await conn->async_execute(
+ mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id),
+ result,
+ diag,
+ asio::as_tuple
+ );
+ if (ec2)
+ {
+ log_error("Error running query", ec2, diag);
+ co_return "ERROR";
+ }
+
+ // Compose the message to be sent back to the client
+ if (result.rows().empty())
+ {
+ co_return "NOT_FOUND";
+ }
+ else
+ {
+ const auto& emp = result.rows()[0];
+ co_return emp.first_name + ' ' + emp.last_name;
+ }
+
+ // When the pooled_connection is destroyed, the connection is returned
+ // to the pool, so it can be re-used.
+}
+//]
+
+//[tutorial_error_handling_session
+asio::awaitable handle_session(mysql::connection_pool& pool, asio::ip::tcp::socket client_socket)
+{
+ // Enable the use of the "s" suffix for std::chrono::seconds
+ using namespace std::chrono_literals;
+
+ //[tutorial_error_handling_read_timeout
+ // Read the request from the client.
+ // async_read ensures that the 8-byte buffer is filled, handling partial reads.
+ // Error the read if it hasn't completed after 30 seconds.
+ unsigned char message[8]{};
+ auto [ec1, bytes_read] = co_await asio::async_read(
+ client_socket,
+ asio::buffer(message),
+ asio::cancel_after(30s, asio::as_tuple)
+ );
+ if (ec1)
+ {
+ // An error or a timeout happened.
+ log_error("Error reading from the socket", ec1);
+ co_return;
+ }
+ //]
+
+ // Parse the 64-bit big-endian int into a native int64_t
+ std::int64_t employee_id = boost::endian::load_big_s64(message);
+
+ //[tutorial_error_handling_db_timeout
+ // Invoke the database handling logic.
+ // Apply an overall timeout of 20 seconds to the entire coroutine.
+ // Using asio::co_spawn allows us to pass a completion token, like asio::cancel_after.
+ // As other async operations, co_spawn's default completion token allows
+ // us to use co_await on its return value.
+ std::string response = co_await asio::co_spawn(
+ // Run the child coroutine using the same executor as this coroutine
+ co_await asio::this_coro::executor,
+
+ // The coroutine should run our database logic
+ [&pool, employee_id] { return get_employee_details(pool, employee_id); },
+
+ // Apply a timeout, and return an object that can be co_awaited.
+ // We don't use as_tuple here because we're already handling I/O errors
+ // inside get_employee_details. If an unexpected exception happens, propagate it.
+ asio::cancel_after(20s)
+ );
+ //]
+
+ // Write the response back to the client.
+ // async_write ensures that the entire message is written, handling partial writes.
+ // Set a timeout to the write operation, too.
+ auto [ec2, bytes_written] = co_await asio::async_write(
+ client_socket,
+ asio::buffer(response),
+ asio::cancel_after(30s, asio::as_tuple)
+ );
+ if (ec2)
+ {
+ log_error("Error writing to the socket", ec2);
+ co_return;
+ }
+
+ // The socket's destructor will close the client connection
+}
+//]
+
+asio::awaitable listener(mysql::connection_pool& pool, unsigned short port)
+{
+ // An object that accepts incoming TCP connections.
+ asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor);
+
+ // The endpoint where the server will listen.
+ asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port);
+
+ // Open the acceptor
+ acc.open(listening_endpoint.protocol());
+
+ // Allow reusing the local address, so we can restart our server
+ // without encountering errors in bind
+ acc.set_option(asio::socket_base::reuse_address(true));
+
+ // Bind to the local address
+ acc.bind(listening_endpoint);
+
+ // Start listening for connections
+ acc.listen();
+ std::cout << "Server listening at " << acc.local_endpoint() << std::endl;
+
+ // Start the accept loop
+ while (true)
+ {
+ // Accept a new connection
+ auto [ec, sock] = co_await acc.async_accept(asio::as_tuple);
+ if (ec)
+ {
+ log_error("Error accepting connection", ec);
+ co_return;
+ }
+
+ // Function implementing our session logic.
+ // Take ownership of the socket.
+ // Having this as a named variable workarounds a gcc bug
+ // (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288)
+ auto session_logic = [&pool, s = std::move(sock)]() mutable {
+ return handle_session(pool, std::move(s));
+ };
+
+ // Launch a coroutine that runs our session logic.
+ // We don't co_await this coroutine so we can listen
+ // to new connections while the session is running
+ asio::co_spawn(
+ // Use the same executor as the current coroutine
+ co_await asio::this_coro::executor,
+
+ // Session logic
+ std::move(session_logic),
+
+ // Will be called when the coroutine finishes
+ [](std::exception_ptr ptr) {
+ if (ptr)
+ {
+ // For extra safety, log the exception but don't propagate it.
+ // If we failed to anticipate an error condition that ends up raising an exception,
+ // terminate only the affected session, instead of crashing the server.
+ try
+ {
+ std::rethrow_exception(ptr);
+ }
+ catch (const std::exception& exc)
+ {
+ std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
+ }
+ }
+ }
+ );
+ }
+}
+
+void main_impl(int argc, char** argv)
+{
+ if (argc != 5)
+ {
+ std::cerr << "Usage: " << argv[0] << " \n";
+ exit(1);
+ }
+
+ const char* username = argv[1];
+ const char* password = argv[2];
+ const char* server_hostname = argv[3];
+ auto listener_port = static_cast(std::stoi(argv[4]));
+
+ // Create an I/O context, required by all I/O objects
+ asio::io_context ctx;
+
+ // pool_params contains configuration for the pool.
+ // You must specify enough information to establish a connection,
+ // including the server address and credentials.
+ // You can configure a lot of other things, like pool limits
+ mysql::pool_params params;
+ params.server_address.emplace_host_and_port(server_hostname);
+ params.username = username;
+ params.password = password;
+ params.database = "boost_mysql_examples";
+
+ // Construct the pool.
+ // ctx will be used to create the connections and other I/O objects
+ mysql::connection_pool pool(ctx, std::move(params));
+
+ // You need to call async_run on the pool before doing anything useful with it.
+ // async_run creates connections and keeps them healthy. It must be called
+ // only once per pool.
+ // The detached completion token means that we don't want to be notified when
+ // the operation ends. It's similar to a no-op callback.
+ pool.async_run(asio::detached);
+
+ // signal_set is an I/O object that allows waiting for signals
+ asio::signal_set signals(ctx, SIGINT, SIGTERM);
+
+ // Wait for signals
+ signals.async_wait([&](boost::system::error_code, int) {
+ // Stop the execution context. This will cause io_context::run to return
+ ctx.stop();
+ });
+
+ // Launch our listener
+ asio::co_spawn(
+ ctx,
+ [&pool, listener_port] { return listener(pool, listener_port); },
+ // If any exception is thrown in the coroutine body, rethrow it.
+ [](std::exception_ptr ptr) {
+ if (ptr)
+ {
+ std::rethrow_exception(ptr);
+ }
+ }
+ );
+
+ // Calling run will actually execute the coroutine until completion
+ ctx.run();
+}
+
+int main(int argc, char** argv)
+{
+ try
+ {
+ main_impl(argc, argv);
+ }
+ catch (const std::exception& err)
+ {
+ std::cerr << "Error: " << err.what() << std::endl;
+ return 1;
+ }
+}
+
+//]
+
+#else
+
+#include
+
+int main()
+{
+ std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example"
+ << std::endl;
+}
+
+#endif
diff --git a/example/2_simple/timeouts.cpp b/example/2_simple/deletes.cpp
similarity index 52%
rename from example/2_simple/timeouts.cpp
rename to example/2_simple/deletes.cpp
index 6078e1088..c71dbcc10 100644
--- a/example/2_simple/timeouts.cpp
+++ b/example/2_simple/deletes.cpp
@@ -8,86 +8,79 @@
#include
#ifdef BOOST_ASIO_HAS_CO_AWAIT
-//[example_timeouts
+//[example_deletes
/**
- * This example demonstrates how to set a timeout to your async operations
- * using asio::cancel_after. We will set a timeout to an individual query,
- * as well as to an entire coroutine. cancel_after can be used with any
- * Boost.Asio-compliant async function.
+ * This example demonstrates how to use DELETE statements
+ * and the results::affected_rows() function.
*
- * This example uses C++20 coroutines. If you need, you can backport
- * it to C++11 by using callbacks or asio::yield_context.
- * Timeouts can't be used with sync functions.
+ * The program deletes an employee, given their ID,
+ * and prints whether the deletion was successful.
+ *
+ * It uses C++20 coroutines. If you need, you can backport
+ * it to C++11 by using callbacks, asio::yield_context
+ * or sync functions instead of coroutines.
+ *
+ * This example uses the 'boost_mysql_examples' database, which you
+ * can get by running db_setup.sql.
*/
#include
-#include
+#include
#include
-#include
#include
#include
-#include
#include
#include
-#include
-#include
+#include
#include
+#include
#include
-namespace asio = boost::asio;
namespace mysql = boost::mysql;
-
-void print_employee(mysql::row_view employee)
-{
- std::cout << "Employee '" << employee.at(0) << " " // first_name (string)
- << employee.at(1) << "' earns " // last_name (string)
- << employee.at(2) << " dollars yearly\n"; // salary (double)
-}
+namespace asio = boost::asio;
// The main coroutine
asio::awaitable coro_main(
std::string_view server_hostname,
std::string_view username,
std::string_view password,
- std::string_view company_id
+ std::int64_t employee_id
)
{
// Create a connection.
// Will use the same executor as the coroutine.
mysql::any_connection conn(co_await asio::this_coro::executor);
- // The hostname, username, password and database to use
+ // The server host, username, password and database to use.
mysql::connect_params params;
params.server_address.emplace_host_and_port(std::string(server_hostname));
- params.username = username;
- params.password = password;
+ params.username = std::move(username);
+ params.password = std::move(password);
params.database = "boost_mysql_examples";
- // Connect to server
+ // Connect to the server
co_await conn.async_connect(params);
- // Execute the query. company_id is untrusted, so we use with_params.
- // We set a timeout to this query by using asio::cancel_after.
- // On timeout, the operation will fail with asio::error::operation_aborted.
- // You can use asio::cancel_after with any async operation.
- // After a timeout happens, the connection needs to be re-connected.
+ // Perform the deletion.
mysql::results result;
co_await conn.async_execute(
- mysql::with_params(
- "SELECT first_name, last_name, salary FROM employee WHERE company_id = {}",
- company_id
- ),
- result,
- asio::cancel_after(std::chrono::seconds(5))
+ mysql::with_params("DELETE FROM employee WHERE id = {}", employee_id),
+ result
);
- // Print all the obtained rows
- for (boost::mysql::row_view employee : result.rows())
+ // affected_rows() returns the number of rows that were affected
+ // by the executed statement. If there was an affected row, the deletion was successful.
+ // Note that this may not work for UPDATEs, as they may match but not affected some rows.
+ if (result.affected_rows() != 0u)
+ {
+ std::cout << "Deletion successful\n";
+ }
+ else
{
- print_employee(employee);
+ std::cout << "No employee with such ID\n";
}
// Notify the MySQL server we want to quit, then close the underlying connection.
@@ -98,29 +91,24 @@ void main_impl(int argc, char** argv)
{
if (argc != 5)
{
- std::cerr << "Usage: " << argv[0] << " \n";
+ std::cerr << "Usage: " << argv[0] << " \n";
exit(1);
}
// Create an I/O context, required by all I/O objects
asio::io_context ctx;
- // Launch our coroutine with a timeout.
- // If the entire operation hasn't finished before the timeout,
- // the operation being executed at that point will get cancelled,
- // and the entire coroutine will fail with asio::error::operation_aborted
+ // Launch our coroutine
asio::co_spawn(
ctx,
- [=] { return coro_main(argv[3], argv[1], argv[2], argv[4]); },
- asio::cancel_after(
- std::chrono::seconds(20),
- [](std::exception_ptr ptr) {
- if (ptr)
- {
- std::rethrow_exception(ptr);
- }
+ [=] { return coro_main(argv[3], argv[1], argv[2], std::stoi(argv[4])); },
+ // If any exception is thrown in the coroutine body, rethrow it.
+ [](std::exception_ptr ptr) {
+ if (ptr)
+ {
+ std::rethrow_exception(ptr);
}
- )
+ }
);
// Calling run will actually execute the coroutine until completion
@@ -141,7 +129,7 @@ int main(int argc, char** argv)
// Security note: diagnostics::server_message may contain user-supplied values (e.g. the
// field value that caused the error) and is encoded using to the connection's character set
// (UTF-8 by default). Treat is as untrusted input.
- std::cerr << "Error: " << err.what() << '\n'
+ std::cerr << "Error: " << err.what() << ", error code: " << err.code() << '\n'
<< "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl;
return 1;
}
@@ -164,4 +152,4 @@ int main()
<< std::endl;
}
-#endif
\ No newline at end of file
+#endif
diff --git a/example/2_simple/inserts.cpp b/example/2_simple/inserts.cpp
new file mode 100644
index 000000000..b60c112e9
--- /dev/null
+++ b/example/2_simple/inserts.cpp
@@ -0,0 +1,180 @@
+//
+// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+
+#include
+#ifdef BOOST_ASIO_HAS_CO_AWAIT
+
+//[example_inserts
+
+/**
+ * This example demonstrates how to use INSERT statements,
+ * the results::last_insert_id() function, and optionals
+ * to represent potentially NULL values.
+ *
+ * The program inserts an employee, given their first name,
+ * last name and company ID. It then prints the ID of the newly
+ * inserted employee.
+ *
+ * It uses C++20 coroutines. If you need, you can backport
+ * it to C++11 by using callbacks, asio::yield_context
+ * or sync functions instead of coroutines.
+ *
+ * This example uses the 'boost_mysql_examples' database, which you
+ * can get by running db_setup.sql.
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace mysql = boost::mysql;
+namespace asio = boost::asio;
+
+// The main coroutine
+asio::awaitable coro_main(
+ std::string_view server_hostname,
+ std::string_view username,
+ std::string_view password,
+ std::string_view first_name,
+ std::string_view last_name,
+ std::string_view company_id,
+ std::optional salary // empty optional means that a NULL value should be inserted
+)
+{
+ // Create a connection.
+ // Will use the same executor as the coroutine.
+ mysql::any_connection conn(co_await asio::this_coro::executor);
+
+ // The server host, username, password and database to use.
+ mysql::connect_params params;
+ params.server_address.emplace_host_and_port(std::string(server_hostname));
+ params.username = std::move(username);
+ params.password = std::move(password);
+ params.database = "boost_mysql_examples";
+
+ // Connect to the server
+ co_await conn.async_connect(params);
+
+ // Perform the insertion.
+ // If salary is empty, the last {} will be replaced by NULL.
+ mysql::results result;
+ co_await conn.async_execute(
+ mysql::with_params(
+ "INSERT INTO employee (first_name, last_name, company_id, salary) VALUES ({}, {}, {}, {})",
+ first_name,
+ last_name,
+ company_id,
+ salary
+ ),
+ result
+ );
+
+ // results::last_insert_id retrieves the value of the latest
+ // AUTO_INCREMENT field generated by the executed query, if any.
+ // In this case, this is the generated employee_id.
+ // If we needed the entire generated employee, we'd need a transaction
+ // and multi-queries.
+ std::cout << "Successfully created employee with ID: " << result.last_insert_id() << std::endl;
+
+ // Notify the MySQL server we want to quit, then close the underlying connection.
+ co_await conn.async_close();
+}
+
+void main_impl(int argc, char** argv)
+{
+ if (argc < 7 || argc > 8)
+ {
+ std::cerr
+ << "Usage: " << argv[0]
+ << " []\n";
+ exit(1);
+ }
+
+ // In DB, salary is an UNSIGNED INT (32-bit) representing employee salary in USD
+ // It may be NULL (e.g. for contractors).
+ // Parse the command line argument, if present, and validate it's within a sane range
+ std::optional salary;
+ if (argc == 8)
+ {
+ int parsed_salary = std::stoi(argv[7]);
+ if (parsed_salary < 10000 || parsed_salary >= 1000000)
+ {
+ std::cerr << "Salary should be between 10000 and 1000000\n";
+ exit(1);
+ }
+ salary = static_cast(parsed_salary);
+ }
+
+ // Create an I/O context, required by all I/O objects
+ asio::io_context ctx;
+
+ // Launch our coroutine
+ asio::co_spawn(
+ ctx,
+ [=] { return coro_main(argv[3], argv[1], argv[2], argv[4], argv[5], argv[6], salary); },
+ // If any exception is thrown in the coroutine body, rethrow it.
+ [](std::exception_ptr ptr) {
+ if (ptr)
+ {
+ std::rethrow_exception(ptr);
+ }
+ }
+ );
+
+ // Calling run will actually execute the coroutine until completion
+ ctx.run();
+
+ std::cout << "Done\n";
+}
+
+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 character set
+ // (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;
+ }
+}
+
+//]
+
+#else
+
+#include
+
+int main()
+{
+ std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example"
+ << std::endl;
+}
+
+#endif
diff --git a/example/3_advanced/connection_pool/server.cpp b/example/3_advanced/connection_pool/server.cpp
index ae2a681a2..368a7162f 100644
--- a/example/3_advanced/connection_pool/server.cpp
+++ b/example/3_advanced/connection_pool/server.cpp
@@ -163,7 +163,7 @@ error_code notes::launch_server(
{
error_code ec;
- // An object that allows us to acept incoming TCP connections.
+ // An object that allows us to accept incoming TCP connections.
// Since we're in a multi-threaded environment, we create a strand for the acceptor,
// so all accept handlers are run serialized
auto acceptor = std::make_shared(asio::make_strand(ex));
diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index e91fe083b..681844e28 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -65,12 +65,19 @@ endfunction()
set(REGULAR_ARGS example_user example_password ${SERVER_HOST})
# Tutorials
-add_tutorial(tutorial_sync 1_sync.cpp ARGS ${REGULAR_ARGS})
-add_tutorial(tutorial_async 2_async.cpp ARGS ${REGULAR_ARGS})
-add_tutorial(tutorial_with_params 3_with_params.cpp ARGS ${REGULAR_ARGS} 1)
-add_tutorial(tutorial_static_interface 4_static_interface.cpp ARGS ${REGULAR_ARGS} 1 LIBS Boost::pfr)
+add_tutorial(tutorial_sync 1_sync.cpp ARGS ${REGULAR_ARGS})
+add_tutorial(tutorial_async 2_async.cpp ARGS ${REGULAR_ARGS})
+add_tutorial(tutorial_with_params 3_with_params.cpp ARGS ${REGULAR_ARGS} 1)
+add_tutorial(tutorial_static_interface 4_static_interface.cpp ARGS ${REGULAR_ARGS} 1 LIBS Boost::pfr)
+add_tutorial(tutorial_updates_transactions 5_updates_transactions.cpp ARGS ${REGULAR_ARGS} 1 "John" LIBS Boost::pfr)
+add_tutorial(tutorial_connection_pool 6_connection_pool.cpp ARGS ${SERVER_HOST} LIBS Boost::pfr
+ PYTHON_RUNNER run_tutorial_connection_pool.py)
+add_tutorial(tutorial_error_handling 7_error_handling.cpp ARGS ${SERVER_HOST} --test-errors LIBS Boost::pfr
+ PYTHON_RUNNER run_tutorial_connection_pool.py)
# Simple
+add_simple_example(inserts ARGS ${REGULAR_ARGS} "John" "Doe" "HGS")
+add_simple_example(deletes ARGS ${REGULAR_ARGS} 20)
add_simple_example(callbacks ARGS ${REGULAR_ARGS})
add_simple_example(coroutines_cpp11 ARGS ${REGULAR_ARGS} LIBS Boost::context)
add_simple_example(batch_inserts ARGS ${SERVER_HOST} PYTHON_RUNNER run_batch_inserts.py LIBS Boost::json)
@@ -81,10 +88,8 @@ add_simple_example(disable_tls ARGS ${REGULAR_ARGS})
add_simple_example(tls_certificate_verification ARGS ${REGULAR_ARGS})
add_simple_example(metadata ARGS ${REGULAR_ARGS})
add_simple_example(prepared_statements ARGS ${REGULAR_ARGS} "HGS")
-add_simple_example(timeouts ARGS ${REGULAR_ARGS} "HGS")
add_simple_example(pipeline ARGS ${REGULAR_ARGS} "HGS")
add_simple_example(multi_function ARGS ${REGULAR_ARGS})
-add_simple_example(multi_queries_transactions ARGS ${REGULAR_ARGS} 1 "John")
add_simple_example(source_script ARGS ${REGULAR_ARGS} ${CMAKE_CURRENT_SOURCE_DIR}/private/test_script.sql)
# UNIX sockets. Don't run the example on Windows machines
diff --git a/example/Jamfile b/example/Jamfile
index 5aed45a68..96422adfa 100644
--- a/example/Jamfile
+++ b/example/Jamfile
@@ -17,95 +17,106 @@ if $(hostname) = ""
hostname = "127.0.0.1" ;
}
-# Builds and run a "regular" example
-rule run_regular_example (
+# Builds and run an example
+rule run_example (
example_name :
sources * :
- args *
+ args * :
+ python_runner ? :
+ requirements *
)
{
+ # If we're using a Python runner, don't use Valgrind
+ local valgrind_target = /boost/mysql/test//launch_with_valgrind ;
+ local launcher = ;
+ if python_runner {
+ valgrind_target = ;
+ launcher = "python $(this_dir)/private/$(python_runner)" ;
+ }
+
+ # Join the supplied command-line arguments
local arg_str = [ sequence.join $(args) : " " ] ;
+
run
/boost/mysql/test//boost_mysql_compiled
- /boost/mysql/test//launch_with_valgrind
+ $(valgrind_target)
$(sources)
: requirements
$(arg_str)
+ $(launcher)
+ $(requirements)
: target-name $(example_name)
;
}
-# Builds and runs a example that needs a Python runner
-rule run_python_example (
- example_name :
- python_runner :
- sources *
-)
-{
- run
- $(sources)
- /boost/mysql/test//boost_mysql_compiled
- : requirements
- "python $(this_dir)/private/$(python_runner)"
- $(hostname)
- : target-name $(example_name)
- ;
-}
local regular_args = example_user example_password $(hostname) ;
# Tutorials
-run_regular_example tutorial_sync : 1_tutorial/1_sync.cpp : $(regular_args) ;
-run_regular_example tutorial_async : 1_tutorial/2_async.cpp : $(regular_args) ;
-run_regular_example tutorial_with_params : 1_tutorial/3_with_params.cpp : $(regular_args) 1 ;
-run_regular_example tutorial_static_interface : 1_tutorial/4_static_interface.cpp : $(regular_args) 1 ;
+run_example tutorial_sync : 1_tutorial/1_sync.cpp : $(regular_args) ;
+run_example tutorial_async : 1_tutorial/2_async.cpp : $(regular_args) ;
+run_example tutorial_with_params : 1_tutorial/3_with_params.cpp : $(regular_args) 1 ;
+run_example tutorial_static_interface : 1_tutorial/4_static_interface.cpp : $(regular_args) 1 ;
+run_example tutorial_updates_transactions : 1_tutorial/5_updates_transactions.cpp : $(regular_args) 1 "John" ;
+run_example tutorial_connection_pool : 1_tutorial/6_connection_pool.cpp : $(hostname)
+ : run_tutorial_connection_pool.py ;
+run_example tutorial_error_handling : 1_tutorial/7_error_handling.cpp : $(hostname) --test-errors
+ : run_tutorial_connection_pool.py : ;
# Simple examples
-run_regular_example callbacks : 2_simple/callbacks.cpp : $(regular_args) ;
-run_regular_example coroutines_cpp11 : 2_simple/coroutines_cpp11.cpp /boost/mysql/test//boost_context_lib : $(regular_args) ;
-run_python_example batch_inserts : run_batch_inserts.py : 2_simple/batch_inserts.cpp /boost/mysql/test//boost_json_lib ;
-run_python_example batch_inserts_generic : run_batch_inserts.py : 2_simple/batch_inserts_generic.cpp /boost/mysql/test//boost_json_lib ;
-run_python_example patch_updates : run_patch_updates.py : 2_simple/patch_updates.cpp ;
-run_python_example dynamic_filters : run_dynamic_filters.py : 2_simple/dynamic_filters.cpp /boost/mysql/test//boost_context_lib ;
-run_regular_example disable_tls : 2_simple/disable_tls.cpp : $(regular_args) ;
-run_regular_example tls_certificate_verification : 2_simple/tls_certificate_verification.cpp : $(regular_args) ;
-run_regular_example metadata : 2_simple/metadata.cpp : $(regular_args) ;
-run_regular_example prepared_statements : 2_simple/prepared_statements.cpp : $(regular_args) "HGS" ;
-run_regular_example timeouts : 2_simple/timeouts.cpp : $(regular_args) "HGS" ;
-run_regular_example pipeline : 2_simple/pipeline.cpp : $(regular_args) "HGS" ;
-run_regular_example multi_function : 2_simple/multi_function.cpp : $(regular_args) ;
-run_regular_example multi_queries_transactions : 2_simple/multi_queries_transactions.cpp : $(regular_args) 1 "John" ;
-run_regular_example source_script : 2_simple/source_script.cpp : $(regular_args) $(this_dir)/private/test_script.sql ;
-
-# UNIX. Don't run under Windows systems
-run
- 2_simple/unix_socket.cpp
- /boost/mysql/test//boost_mysql_compiled
- /boost/mysql/test//launch_with_valgrind
-:
- requirements
+run_example inserts : 2_simple/inserts.cpp
+ : $(regular_args) "John" "Doe" "HGS" 50000 ;
+run_example deletes : 2_simple/deletes.cpp
+ : $(regular_args) 20 ;
+run_example callbacks : 2_simple/callbacks.cpp
+ : $(regular_args) ;
+run_example coroutines_cpp11 : 2_simple/coroutines_cpp11.cpp /boost/mysql/test//boost_context_lib
+ : $(regular_args) ;
+run_example batch_inserts : 2_simple/batch_inserts.cpp /boost/mysql/test//boost_json_lib
+ : $(hostname) : run_batch_inserts.py ;
+run_example batch_inserts_generic : 2_simple/batch_inserts_generic.cpp /boost/mysql/test//boost_json_lib
+ : $(hostname) : run_batch_inserts.py ;
+run_example patch_updates : 2_simple/patch_updates.cpp
+ : $(hostname) : run_patch_updates.py ;
+run_example dynamic_filters : 2_simple/dynamic_filters.cpp
+ : $(hostname) : run_dynamic_filters.py ;
+run_example disable_tls : 2_simple/disable_tls.cpp
+ : $(regular_args) ;
+run_example tls_certificate_verification : 2_simple/tls_certificate_verification.cpp
+ : $(regular_args) ;
+run_example metadata : 2_simple/metadata.cpp
+ : $(regular_args) ;
+run_example prepared_statements : 2_simple/prepared_statements.cpp
+ : $(regular_args) "HGS" ;
+run_example multi_function : 2_simple/multi_function.cpp
+ : $(regular_args) ;
+run_example pipeline : 2_simple/pipeline.cpp
+ : $(regular_args) "HGS" ;
+run_example source_script : 2_simple/source_script.cpp
+ : $(regular_args) $(this_dir)/private/test_script.sql ;
+run_example unix_socket : 2_simple/unix_socket.cpp
+ : example_user example_password
+ :
+ :
windows:no
- "example_user example_password"
-;
+ ;
-# Connection pool
-run
+# Advanced
+run_example connection_pool :
3_advanced/connection_pool/main.cpp
3_advanced/connection_pool/repository.cpp
3_advanced/connection_pool/handle_request.cpp
3_advanced/connection_pool/server.cpp
- /boost/mysql/test//boost_mysql_compiled
/boost/mysql/test//boost_context_lib
/boost/mysql/test//boost_json_lib
/boost/url//boost_url
/boost/mysql/test//boost_beast_lib
- : requirements
- "python $(this_dir)/private/run_connection_pool.py"
- $(hostname)
- # MSVC 14.1 fails with an internal compiler error while building server.cpp for this config
- msvc-14.1,32,17,release:no
- # Uses heavily Boost.Context coroutines, which aren't fully supported by asan
- norecover:no
- enable:no
- : target-name boost_mysql_example_connection_pool
+ : $(hostname)
+ : run_connection_pool.py
+ :
+ # MSVC 14.1 fails with an internal compiler error while building server.cpp for this config
+ msvc-14.1,32,17,release:no
+ # Uses heavily Boost.Context coroutines, which aren't fully supported by asan
+ norecover:no
+ enable:no
;
diff --git a/example/private/run_connection_pool.py b/example/private/run_connection_pool.py
index be22f6b31..53c1d2493 100644
--- a/example/private/run_connection_pool.py
+++ b/example/private/run_connection_pool.py
@@ -9,13 +9,10 @@
import requests
import random
import argparse
-from subprocess import PIPE, Popen
+from subprocess import PIPE, STDOUT, Popen
from contextlib import contextmanager
import re
-import signal
import os
-import time
-import sys
_is_win = os.name == 'nt'
@@ -42,7 +39,7 @@ def _parse_server_start_line(line: str) -> int:
def _launch_server(exe: str, host: str):
# Launch server and let it choose a free port for us.
# This prevents port clashes during b2 parallel test runs
- server = Popen([exe, 'example_user', 'example_password', host, '0'], stdout=PIPE)
+ server = Popen([exe, 'example_user', 'example_password', host, '0'], stdout=PIPE, stderr=STDOUT)
assert server.stdout is not None
with server:
try:
@@ -134,7 +131,6 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('executable')
parser.add_argument('host')
- parser.add_argument('--nofork', default=None)
args = parser.parse_args()
# Launch the server
diff --git a/example/private/run_tutorial_connection_pool.py b/example/private/run_tutorial_connection_pool.py
new file mode 100644
index 000000000..7049193e3
--- /dev/null
+++ b/example/private/run_tutorial_connection_pool.py
@@ -0,0 +1,121 @@
+#!/usr/bin/python3
+#
+# Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+#
+# Distributed under the Boost Software License, Version 1.0. (See accompanying
+# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+#
+
+import argparse
+from subprocess import PIPE, STDOUT, Popen
+from contextlib import contextmanager
+import re
+import os
+import socket
+import struct
+
+_is_win = os.name == 'nt'
+
+
+# Returns the port the server is listening at
+def _parse_server_start_line(line: str) -> int:
+ m = re.match(r'Server listening at 0\.0\.0\.0:([0-9]+)', line)
+ if m is None:
+ raise RuntimeError('Unexpected server start line')
+ return int(m.group(1))
+
+
+@contextmanager
+def _launch_server(exe: str, host: str):
+ # Launch server and let it choose a free port for us.
+ # This prevents port clashes during b2 parallel test runs
+ server = Popen([exe, 'example_user', 'example_password', host, '0'], stdout=PIPE, stderr=STDOUT)
+ assert server.stdout is not None
+ with server:
+ try:
+ # Wait until the server is ready
+ ready_line = server.stdout.readline().decode()
+ print(ready_line, end='', flush=True)
+ if ready_line.startswith('Sorry'): # C++ standard unsupported, skip the test
+ exit(0)
+ yield _parse_server_start_line(ready_line)
+ finally:
+ print('Terminating server...', flush=True)
+
+ # In Windows, there is no sane way to cleanly terminate the process.
+ # Sending a Ctrl-C terminates all process attached to the console (including ourselves
+ # and any parent test runner). Running the process in a separate terminal doesn't allow
+ # access to stdout, which is problematic, too.
+ if _is_win:
+ # kill is an alias for TerminateProcess with the given exit code
+ os.kill(server.pid, 9999)
+ else:
+ # Send SIGTERM
+ server.terminate()
+
+ # Print any output the process generated
+ print('Server stdout: \n', server.stdout.read().decode(), flush=True)
+
+ # Verify that it exited gracefully
+ if (_is_win and server.returncode != 9999) or (not _is_win and server.returncode):
+ raise RuntimeError('Server did not exit cleanly. retcode={}'.format(server.returncode))
+
+
+class _Runner:
+ def __init__(self, port: int) -> None:
+ self._port = port
+
+ def _connect(self) -> socket.socket:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(('127.0.0.1', self._port))
+ return sock
+
+
+ def _query_employee(self, employee_id: int) -> str:
+ # Open a connection
+ sock = self._connect()
+
+ # Send the request
+ sock.send(struct.pack('>Q', employee_id))
+
+ # Receive the response. It should always fit in a single TCP segment
+ # for the values we have in CI
+ res = sock.recv(4096).decode()
+ assert len(res) > 0
+ return res
+
+
+ def _generate_error(self) -> None:
+ # Open a connection
+ sock = self._connect()
+
+ # Send an incomplete message
+ sock.send(b'abc')
+ sock.close()
+
+
+ def run(self, test_errors: bool) -> None:
+ # Generate an error first. The server should not terminate
+ if test_errors:
+ self._generate_error()
+ assert self._query_employee(1) != 'NOT_FOUND'
+ value = self._query_employee(0xffffffff)
+ assert value == 'NOT_FOUND', 'Value is: {}'.format(value)
+
+
+def main():
+ # Parse command line arguments
+ parser = argparse.ArgumentParser()
+ parser.add_argument('executable')
+ parser.add_argument('host')
+ parser.add_argument('--test-errors', action='store_true')
+ args = parser.parse_args()
+
+ # Launch the server
+ with _launch_server(args.executable, args.host) as listening_port:
+ # Run the tests
+ _Runner(listening_port).run(args.test_errors)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/Jamfile b/test/Jamfile
index 5b489f63d..b99c3b027 100644
--- a/test/Jamfile
+++ b/test/Jamfile
@@ -140,7 +140,7 @@ alias boost_context_lib
:
/boost/context//boost_context/off
: usage-requirements
- # gcc-14+ seem to enable CET by default, which causes warnings with Boost.Context.
+ # gcc-13+ seem to enable CET by default, which causes warnings with Boost.Context.
# Disable CET until https://github.com/boostorg/context/issues/263 gets fixed
gcc-13:-fcf-protection=none
gcc-14:-fcf-protection=none
diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt
index f8fcfbbcc..07d83fc68 100644
--- a/test/integration/CMakeLists.txt
+++ b/test/integration/CMakeLists.txt
@@ -47,6 +47,7 @@ add_executable(
test/snippets/sql_formatting.cpp
test/snippets/sql_formatting_custom.cpp
test/snippets/pipeline.cpp
+ test/snippets/templated_connection.cpp
)
target_include_directories(
boost_mysql_integrationtests
diff --git a/test/integration/Jamfile b/test/integration/Jamfile
index 30390bdf3..10e49d0fd 100644
--- a/test/integration/Jamfile
+++ b/test/integration/Jamfile
@@ -57,6 +57,7 @@ run
test/snippets/sql_formatting.cpp
test/snippets/sql_formatting_custom.cpp
test/snippets/pipeline.cpp
+ test/snippets/templated_connection.cpp
: requirements
include
diff --git a/test/integration/test/snippets/charsets.cpp b/test/integration/test/snippets/charsets.cpp
index a00f85c36..893035007 100644
--- a/test/integration/test/snippets/charsets.cpp
+++ b/test/integration/test/snippets/charsets.cpp
@@ -13,6 +13,8 @@
#include
#include
+namespace mysql = boost::mysql;
+
namespace {
//[charsets_next_char
@@ -67,7 +69,7 @@ BOOST_AUTO_TEST_CASE(section_charsets)
{
{
// Verify that utf8mb4_next_char can be used in a character_set
- boost::mysql::character_set charset{"utf8mb4", utf8mb4_next_char};
+ mysql::character_set charset{"utf8mb4", utf8mb4_next_char};
// It works for valid input
unsigned char buff_valid[] = {0xc3, 0xb1, 0x50};
diff --git a/test/integration/test/snippets/connection_establishment.cpp b/test/integration/test/snippets/connection_establishment.cpp
index 4a7f3a4e6..e13ecad90 100644
--- a/test/integration/test/snippets/connection_establishment.cpp
+++ b/test/integration/test/snippets/connection_establishment.cpp
@@ -7,15 +7,10 @@
#include
#include
-#include
-#include
-#include
#include
#include "test_common/io_context_fixture.hpp"
-#include "test_integration/run_coro.hpp"
-#include "test_integration/snippets/snippets_fixture.hpp"
namespace mysql = boost::mysql;
namespace asio = boost::asio;
@@ -40,19 +35,6 @@ BOOST_FIXTURE_TEST_CASE(section_connection_establishment, io_context_fixture)
// Connect and use the connection normally
//]
}
-#ifdef BOOST_ASIO_HAS_CO_AWAIT
- {
- run_coro(ctx, []() -> asio::awaitable {
- mysql::any_connection conn(co_await asio::this_coro::executor);
- co_await conn.async_connect(snippets_connect_params());
-
- //[section_connection_establishment_multi_queries_execute
- mysql::results result;
- co_await conn.async_execute("START TRANSACTION; SELECT 1; COMMIT", result);
- //]
- });
- }
-#endif
}
} // namespace
diff --git a/test/integration/test/snippets/templated_connection.cpp b/test/integration/test/snippets/templated_connection.cpp
new file mode 100644
index 000000000..a49f83125
--- /dev/null
+++ b/test/integration/test/snippets/templated_connection.cpp
@@ -0,0 +1,179 @@
+//
+// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include "test_common/ci_server.hpp"
+#include "test_common/io_context_fixture.hpp"
+#include "test_integration/run_coro.hpp"
+#include "test_integration/server_features.hpp"
+#include "test_integration/snippets/credentials.hpp"
+
+namespace mysql = boost::mysql;
+namespace asio = boost::asio;
+using namespace mysql::test;
+
+namespace {
+
+BOOST_AUTO_TEST_SUITE(section_templated_connection)
+
+BOOST_AUTO_TEST_CASE(creation)
+{
+ auto server_hostname = get_hostname();
+
+ //[templated_connection_creation
+ // The execution context, required for all I/O operations
+ asio::io_context ctx;
+
+ // The SSL context, required for connections that use TLS.
+ asio::ssl::context ssl_ctx(asio::ssl::context::tlsv12_client);
+
+ // Construct the connection. The arguments are forwarded
+ // to the stream type (asio::ssl::stream).
+ mysql::tcp_ssl_connection conn(ctx, ssl_ctx);
+ //]
+
+ //[templated_connection_connect
+ // Resolve the hostname to get a collection of endpoints.
+ // default_port_string is MySQL's default port, 3306
+ // Hostname resolution may yield more than one host
+ asio::ip::tcp::resolver resolver(ctx);
+ auto endpoints = resolver.resolve(server_hostname, mysql::default_port_string);
+
+ // Parameters specifying how to perform the MySQL handshake operation.
+ // Similar to connect_params, but doesn't contain the server address and is non-owning
+ mysql::handshake_params params(
+ mysql_username,
+ mysql_password,
+ "boost_mysql_examples" // database to use
+ );
+
+ // Connect to the server using the first endpoint returned by the resolver
+ conn.connect(*endpoints.begin(), params);
+ //]
+
+ //[templated_connection_use
+ // Issue a query, as you would with any_connection
+ mysql::results result;
+ conn.execute("SELECT 1", result);
+ //]
+
+ //[templated_connection_close
+ conn.close();
+ //]
+}
+
+#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
+BOOST_TEST_DECORATOR(*run_if(&server_features::unix_sockets))
+BOOST_AUTO_TEST_CASE(unix_sockets)
+{
+ //[templated_connection_unix
+ // The execution context, required for all I/O operations
+ asio::io_context ctx;
+
+ // A UNIX connection requires only an execution context
+ mysql::unix_connection conn(ctx);
+
+ // The socket path where the server is listening
+ asio::local::stream_protocol::endpoint ep("/var/run/mysqld/mysqld.sock");
+
+ // MySQL handshake parameters, as in the TCP case.
+ mysql::handshake_params params(
+ mysql_username,
+ mysql_password,
+ "boost_mysql_examples" // database to use
+ );
+
+ // Connect to the server
+ conn.connect(ep, params);
+
+ // Use the connection normally
+ //]
+}
+#endif
+
+BOOST_AUTO_TEST_CASE(handshake_quit)
+{
+ auto server_hostname = get_hostname();
+
+ //[templated_connection_handshake_quit
+ // The execution context, required for all I/O operations
+ asio::io_context ctx;
+
+ // The SSL context, required for connections that use TLS.
+ asio::ssl::context ssl_ctx(asio::ssl::context::tlsv12_client);
+
+ // We're using TLS over TCP
+ mysql::tcp_ssl_connection conn(ctx, ssl_ctx);
+
+ // Resolve the server hostname into endpoints
+ asio::ip::tcp::resolver resolver(ctx);
+ auto endpoints = resolver.resolve(server_hostname, mysql::default_port_string);
+
+ // Connect the underlying stream manually.
+ // asio::connect tries every endpoint in the passed sequence
+ // until one succeeds. any_connection uses this internally.
+ // lowest_layer obtains the underlying socket from the ssl::stream
+ asio::connect(conn.stream().lowest_layer(), endpoints);
+
+ // Perform MySQL session establishment.
+ // This will also perform the TLS handshake, if required.
+ mysql::handshake_params params(
+ mysql_username,
+ mysql_password,
+ "boost_mysql_examples" // database to use
+ );
+ conn.handshake(params);
+
+ // Use the connection normally
+ mysql::results result;
+ conn.execute("SELECT 1", result);
+
+ // Terminate the connection. This also performs the TLS shutdown.
+ conn.quit();
+
+ // Close the underlying stream.
+ // The connection's destructor also closes the socket,
+ // but doing it explicitly will throw in case of error.
+ conn.stream().lowest_layer().close();
+ //]
+}
+
+#ifdef BOOST_ASIO_HAS_CO_AWAIT
+BOOST_FIXTURE_TEST_CASE(with_diagnostics_, io_context_fixture)
+{
+ run_coro(ctx, [&]() -> asio::awaitable {
+ // Setup
+ mysql::tcp_connection conn(ctx);
+ asio::ip::tcp::resolver resolv(ctx);
+ auto endpoints = co_await resolv.async_resolve(get_hostname(), mysql::default_port_string);
+ mysql::handshake_params hparams(mysql_username, mysql_password);
+ co_await conn.async_connect(*endpoints.begin(), hparams, mysql::with_diagnostics(asio::deferred));
+ mysql::results result;
+
+ //[templated_connection_with_diagnostics
+ co_await conn.async_execute("SELECT 1", result, mysql::with_diagnostics(asio::deferred));
+ //]
+ });
+}
+#endif
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // namespace
diff --git a/test/integration/test/snippets/tutorials.cpp b/test/integration/test/snippets/tutorials.cpp
index 7c652b01e..7a1c17760 100644
--- a/test/integration/test/snippets/tutorials.cpp
+++ b/test/integration/test/snippets/tutorials.cpp
@@ -5,35 +5,445 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
+#include
+
+#include
+#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
+
+#include
+#include
+#include
+#include
+#include
+#include
#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
+#include
+#include
+#include
+
+#include "test_common/ci_server.hpp"
+#include "test_integration/run_coro.hpp"
+#include "test_integration/snippets/credentials.hpp"
#include "test_integration/snippets/snippets_fixture.hpp"
namespace mysql = boost::mysql;
+namespace asio = boost::asio;
using namespace mysql::test;
+// Common
+inline namespace tutorials {
+struct employee
+{
+ std::string first_name;
+ std::string last_name;
+};
+} // namespace tutorials
+
namespace {
-// Taken here because it's only used in the discussion
-void print_employee(mysql::string_view first_name, mysql::string_view last_name)
+//
+// Tutorial 4: static interface
+//
+BOOST_FIXTURE_TEST_CASE(section_tutorial_static_interface, snippets_fixture)
{
- std::cout << "Employee's name is: " << first_name << ' ' << last_name << std::endl;
+ mysql::results result;
+ conn.execute("SELECT first_name, last_name FROM employee WHERE id = 1", result);
+ auto print_employee = [](mysql::string_view, mysql::string_view) {};
+
+ //[tutorial_static_casts
+ mysql::row_view employee = result.rows().at(0);
+ print_employee(employee.at(0).as_string(), employee.at(1).as_string());
+ //]
}
-BOOST_FIXTURE_TEST_CASE(section_tutorials, snippets_fixture)
+//
+// Tutorial 5: updates and txns
+//
+asio::awaitable tutorial_updates_transactions(mysql::any_connection& conn)
{
+ const char* new_first_name = "John";
+ int employee_id = 1;
+
+ {
+ //[tutorial_updates_transactions_update
+ // Run an UPDATE. We can use with_params to compose it, too
+ // If new_first_name contains 'John' and employee_id contains 42, this will run:
+ // UPDATE employee SET first_name = 'John' WHERE id = 42
+ // result contains an empty resultset: it has no rows
+ mysql::results result;
+ co_await conn.async_execute(
+ mysql::with_params(
+ "UPDATE employee SET first_name = {} WHERE id = {}",
+ new_first_name,
+ employee_id
+ ),
+ result
+ );
+ //]
+ //[tutorial_updates_transactions_select
+ // Retrieve the newly created employee.
+ // As we will see, this is a potential race condition
+ // that can be avoided with transactions.
+ co_await conn.async_execute(
+ mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id),
+ result
+ );
+
+ if (result.rows().empty())
+ {
+ std::cout << "No employee with ID = " << employee_id << std::endl;
+ }
+ else
+ {
+ std::cout << "Updated: " << result.rows().at(0).at(0) << " " << result.rows().at(0).at(1)
+ << std::endl;
+ }
+ //]
+ }
+ {
+ //[tutorial_updates_transactions_txn
+ mysql::results empty_result, select_result;
+
+ // Start a transaction block. Subsequent statements will belong
+ // to the transaction block, until a COMMIT or ROLLBACK is encountered,
+ // or the connection is closed.
+ // START TRANSACTION returns no rows.
+ co_await conn.async_execute("START TRANSACTION", empty_result);
+
+ // Run the UPDATE as we did before
+ co_await conn.async_execute(
+ mysql::with_params(
+ "UPDATE employee SET first_name = {} WHERE id = {}",
+ new_first_name,
+ employee_id
+ ),
+ empty_result
+ );
+
+ // Run the SELECT. If a row is returned here, it is the one
+ // that we modified.
+ co_await conn.async_execute(
+ mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id),
+ select_result
+ );
+
+ // Commit the transaction. This makes the updated row visible
+ // to other transactions and releases any locked rows.
+ co_await conn.async_execute("COMMIT", empty_result);
+
+ // Process the retrieved rows
+ if (select_result.rows().empty())
+ {
+ std::cout << "No employee with ID = " << employee_id << std::endl;
+ }
+ else
+ {
+ std::cout << "Updated: " << select_result.rows().at(0).at(0) << " "
+ << select_result.rows().at(0).at(1) << std::endl;
+ }
+ //]
+ }
{
+ //[tutorial_updates_transactions_multi_queries
+ // Run the 4 statements in a single round-trip.
+ // If an error is encountered, successive statements won't be executed
+ // and the transaction won't be committed.
mysql::results result;
- conn.execute("SELECT first_name, last_name FROM employee WHERE id = 1", result);
+ co_await conn.async_execute(
+ mysql::with_params(
+ "START TRANSACTION;"
+ "UPDATE employee SET first_name = {} WHERE id = {};"
+ "SELECT first_name, last_name FROM employee WHERE id = {};"
+ "COMMIT",
+ new_first_name,
+ employee_id,
+ employee_id
+ ),
+ result
+ );
+ //]
- //[tutorial_static_casts
- mysql::row_view employee = result.rows().at(0);
- print_employee(employee.at(0).as_string(), employee.at(1).as_string());
+ //[tutorial_updates_transactions_dynamic_results
+ // Get the 3rd resultset. resultset_view API is similar to results
+ mysql::resultset_view select_result = result.at(2);
+ if (select_result.rows().empty())
+ {
+ std::cout << "No employee with ID = " << employee_id << std::endl;
+ }
+ else
+ {
+ std::cout << "Updated: " << select_result.rows().at(0).at(0) << " "
+ << select_result.rows().at(0).at(1) << std::endl;
+ }
//]
}
+ {
+ //[tutorial_updates_transactions_manual_indices
+ // {0} will be replaced by the first format arg, {1} by the second
+ mysql::results result;
+ co_await conn.async_execute(
+ mysql::with_params(
+ "START TRANSACTION;"
+ "UPDATE employee SET first_name = {0} WHERE id = {1};"
+ "SELECT first_name, last_name FROM employee WHERE id = {1};"
+ "COMMIT",
+ new_first_name,
+ employee_id
+ ),
+ result
+ );
+ //]
+ }
+}
+
+BOOST_FIXTURE_TEST_CASE(section_tutorial_updates_transactions, snippets_fixture)
+{
+ run_coro(ctx, [&]() { return tutorial_updates_transactions(conn); });
+}
+
+//
+// Tutorial 6: connection pool
+//
+mysql::pool_params create_pool_params()
+{
+ mysql::pool_params res;
+ res.server_address.emplace_host_and_port(get_hostname());
+ res.username = mysql_username;
+ res.password = mysql_password;
+ res.database = "boost_mysql_examples";
+ return res;
+}
+
+BOOST_FIXTURE_TEST_CASE(section_tutorial_connection_pool, snippets_fixture)
+{
+ run_coro(ctx, [&]() -> asio::awaitable {
+ mysql::connection_pool pool(ctx, create_pool_params());
+ pool.async_run(asio::detached);
+
+ //[tutorial_connection_pool_get_connection
+ // Get a connection from the pool.
+ // This will wait until a healthy connection is ready to be used.
+ // pooled_connection grants us exclusive access to the connection until
+ // the object is destroyed
+ mysql::pooled_connection conn = co_await pool.async_get_connection();
+ //]
+ });
+}
+
+//
+// Tutorial 7: error handling
+//
+void log_error(const char*, boost::system::error_code) {}
+
+//[tutorial_error_handling_db_nodiag
+asio::awaitable get_employee_details(mysql::connection_pool& pool, std::int64_t employee_id)
+{
+ // Get a connection from the pool.
+ // This will wait until a healthy connection is ready to be used.
+ // ec is an error code, conn is a pooled_connection
+ auto [ec, conn] = co_await pool.async_get_connection(asio::as_tuple);
+ if (ec)
+ {
+ // A connection couldn't be obtained.
+ // This may be because a timeout happened.
+ log_error("Error in async_get_connection", ec);
+ co_return "ERROR";
+ }
+
+ // Use the connection normally to query the database.
+ mysql::static_results> result;
+ auto [ec2] = co_await conn->async_execute(
+ mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id),
+ result,
+ asio::as_tuple
+ );
+ if (ec2)
+ {
+ log_error("Error running query", ec);
+ co_return "ERROR";
+ }
+
+ // Handle the result as we did in the previous tutorial
+ //<-
+ co_return "";
+ //->
+}
+//]
+
+[[maybe_unused]]
+//[tutorial_error_handling_session_as_tuple
+asio::awaitable handle_session(mysql::connection_pool& pool, asio::ip::tcp::socket client_socket)
+{
+ // Read the request from the client.
+ unsigned char message[8]{};
+ auto [ec1, bytes_read] = co_await asio::async_read(client_socket, asio::buffer(message), asio::as_tuple);
+ if (ec1)
+ {
+ log_error("Error reading from the socket", ec1);
+ co_return;
+ }
+
+ // Process the request as before (omitted)
+ //<-
+ boost::ignore_unused(pool);
+ std::string response;
+ //->
+
+ // Write the response back to the client.
+ auto [ec2, bytes_written] = co_await asio::async_write(
+ client_socket,
+ asio::buffer(response),
+ asio::as_tuple
+ );
+ if (ec2)
+ {
+ log_error("Error writing to the socket", ec2);
+ co_return;
+ }
+}
+//]
+
+asio::awaitable tutorial_error_handling()
+{
+ // Setup
+ mysql::connection_pool pool(co_await asio::this_coro::executor, create_pool_params());
+ asio::steady_timer cv(co_await asio::this_coro::executor, (std::chrono::steady_clock::time_point::max)());
+ pool.async_run([&](mysql::error_code) { cv.cancel(); });
+
+ {
+ //[tutorial_error_handling_callbacks
+ // Function to call when async_get_connection completes
+ auto on_available_connection = [](boost::system::error_code ec, mysql::pooled_connection conn) {
+ // Do something useful with the connection
+ //<-
+ BOOST_TEST(ec == mysql::error_code());
+ BOOST_TEST(conn.valid());
+ //->
+ };
+
+ // Start the operation. on_available_connection will be called when the operation
+ // completes. on_available_connection is the completion token.
+ // When a callback is passed, async_get_connection returns void,
+ // so we can't use co_await with it.
+ pool.async_get_connection(on_available_connection);
+ //]
+ }
+
+ {
+ //[tutorial_error_handling_default_tokens
+ // These two lines are equivalent.
+ // Both of them can be read as "I want to use C++20 coroutines as my completion style"
+ auto conn1 = co_await pool.async_get_connection();
+ auto conn2 = co_await pool.async_get_connection(mysql::with_diagnostics(asio::deferred));
+ //]
+
+ BOOST_TEST(conn1.valid());
+ BOOST_TEST(conn2.valid());
+ }
+ {
+ //[tutorial_error_handling_adapter_tokens
+ // Enable the use of the "s" suffix for std::chrono::seconds
+ using namespace std::chrono_literals;
+
+ // The following two lines are equivalent.
+ // Both get a connection, waiting no more than 20s before cancelling the operation.
+ // If no token is passed to cancel_after, the default one will be used,
+ // which transforms the operation into an awaitable.
+ // asio::cancel_after(20s) is usually termed "partial completion token"
+ auto conn1 = co_await pool.async_get_connection(asio::cancel_after(20s));
+ auto conn2 = co_await pool.async_get_connection(
+ asio::cancel_after(20s, mysql::with_diagnostics(asio::deferred))
+ );
+ //]
+
+ BOOST_TEST(conn1.valid());
+ BOOST_TEST(conn2.valid());
+ }
+
+ {
+ //[tutorial_error_handling_as_tuple
+ // Passing asio::as_tuple transforms the operation's handler signature:
+ // Original: void(error_code, mysql::pooled_connection)
+ // Transformed: void(std::tuple)
+ // The transformed signature no longer has an error_code as first parameter,
+ // so no automatic error code to exception transformation happens.
+ std::tuple
+ res = co_await pool.async_get_connection(asio::as_tuple);
+ //]
+
+ BOOST_TEST(std::get<0>(res) == mysql::error_code());
+ }
+
+ {
+ //[tutorial_error_handling_as_tuple_structured_bindings
+ // ec is an error_code, conn is the mysql::pooled_connection.
+ // If the operation fails, ec will be non-empty.
+ auto [ec, conn] = co_await pool.async_get_connection(asio::as_tuple);
+ //]
+
+ BOOST_TEST(ec == mysql::error_code());
+ BOOST_TEST(conn.valid());
+ }
+
+ {
+ //[tutorial_error_handling_as_tuple_default_tokens
+ // The following two lines are equivalent.
+ // Both of them produce an awaitable that produces a tuple when awaited.
+ auto [ec1, conn1] = co_await pool.async_get_connection(asio::as_tuple);
+ auto [ec2, conn2] = co_await pool.async_get_connection(
+ asio::as_tuple(mysql::with_diagnostics(asio::deferred))
+ );
+ //]
+
+ BOOST_TEST(ec1 == mysql::error_code());
+ BOOST_TEST(ec2 == mysql::error_code());
+ BOOST_TEST(conn1.valid());
+ BOOST_TEST(conn2.valid());
+ }
+
+ {
+ using namespace std::chrono_literals;
+
+ //[tutorial_error_handling_as_tuple_cancel_after
+ // ec is an error_code, conn is the mysql::pooled_connection
+ // Apply a timeout and don't throw on error
+ auto [ec, conn] = co_await pool.async_get_connection(asio::cancel_after(20s, asio::as_tuple));
+ //]
+
+ BOOST_TEST(ec == mysql::error_code());
+ BOOST_TEST(conn.valid());
+ }
+
+ // Call the functions requiring a pool
+ co_await get_employee_details(pool, 1);
+
+ // Cancel the pool and wait run to return, so no work is left in the io_context
+ pool.cancel();
+ boost::ignore_unused(co_await cv.async_wait(asio::as_tuple));
+}
+
+BOOST_FIXTURE_TEST_CASE(section_tutorial_error_handling, io_context_fixture)
+{
+ run_coro(ctx, &tutorial_error_handling);
}
} // namespace
+
+#endif
diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py
index 470a6cc74..c16d4e02b 100644
--- a/tools/scripts/examples_qbk.py
+++ b/tools/scripts/examples_qbk.py
@@ -100,16 +100,19 @@ class MultiExample(NamedTuple):
# List all examples here
TUTORIALS = [
- Example('tutorial_sync', '1_tutorial/1_sync.cpp', 'Tutorial 1 listing: hello world!'),
- Example('tutorial_async', '1_tutorial/2_async.cpp', 'Tutorial 2 listing: going async with C++20 coroutines'),
- Example('tutorial_with_params', '1_tutorial/3_with_params.cpp', 'Tutorial 3 listing: queries with parameters'),
- Example('tutorial_static_interface', '1_tutorial/4_static_interface.cpp', 'Tutorial 4 listing: the static interface'),
+ Example('tutorial_sync', '1_tutorial/1_sync.cpp', 'Tutorial 1 listing: hello world!'),
+ Example('tutorial_async', '1_tutorial/2_async.cpp', 'Tutorial 2 listing: going async with C++20 coroutines'),
+ Example('tutorial_with_params', '1_tutorial/3_with_params.cpp', 'Tutorial 3 listing: queries with parameters'),
+ Example('tutorial_static_interface', '1_tutorial/4_static_interface.cpp', 'Tutorial 4 listing: the static interface'),
+ Example('tutorial_updates_transactions', '1_tutorial/5_updates_transactions.cpp', 'Tutorial 5 listing: UPDATEs, transactions and multi-queries'),
+ Example('tutorial_connection_pool', '1_tutorial/6_connection_pool.cpp', 'Tutorial 6 listing: connection pools'),
+ Example('tutorial_error_handling', '1_tutorial/7_error_handling.cpp', 'Tutorial 7 listing: error handling'),
]
SIMPLE_EXAMPLES = [
+ Example('inserts', '2_simple/inserts.cpp', 'INSERTs, last_insert_id() and NULL values'),
+ Example('deletes', '2_simple/deletes.cpp', 'DELETEs and affected_rows()'),
Example('prepared_statements', '2_simple/prepared_statements.cpp', 'Prepared statements'),
- Example('timeouts', '2_simple/timeouts.cpp', 'Setting timeouts to operations'),
- Example('multi_queries_transactions', '2_simple/multi_queries_transactions.cpp', 'Using multi-queries and transactions'),
Example('disable_tls', '2_simple/disable_tls.cpp', 'Disabling TLS for a connection'),
Example('tls_certificate_verification', '2_simple/tls_certificate_verification.cpp', 'Setting TLS options: enabling TLS certificate verification'),
Example('metadata', '2_simple/metadata.cpp', 'Metadata'),