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 types Formattable types Pipeline 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'),