diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 37c046e46..ba6e34a6f 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -7,7 +7,7 @@ [library Boost.MySQL [quickbook 1.7] - [copyright 2019 - 2023 Ruben Perez] + [copyright 2019 - 2024 Ruben Perez] [id mysql] [purpose MySQL client library] [license @@ -56,8 +56,9 @@ [def __Context__ [@boost:/libs/context/index.html Boost.Context]] [def __Self__ [@boost:/libs/mysql/index.html Boost.MySQL]] [def __boost_optional__ [@boost:/libs/optional/index.html `boost::optional`]] -[def __see_error_handling__ See [link mysql.error_handling this section] for more info on error handling.] -[def __assume_setup__ This example assumes you have gone through the [link mysql.examples.setup setup].] +[def __Describe__ [@boost:/libs/describe/index.html Boost.Describe]] +[def __Pfr__ [@boost:/libs/pfr/index.html Boost.Pfr]] +[def __ssl_context__ [asioreflink ssl__context ssl::context]] [/ MySQL stuff] [def __Mysql__ [@https://www.mysql.com/ MySQL]] @@ -65,7 +66,6 @@ [def __allow_invalid_dates__ [mysqllink sql-mode.html#sqlmode_allow_invalid_dates `ALLOW_INVALID_DATES`]] [def __strict_sql__ [mysqllink sql-mode.html#sql-mode-strict strict SQL mode]] [def __time_zone__ [mysqllink server-system-variables.html#sysvar_time_zone `time_zone`]] -[def __SET_NAMES__ [mysqllink set-names.html `SET NAMES`]] [def __TINYINT__ [mysqllink integer-types.html `TINYINT`]] [def __SMALLINT__ [mysqllink integer-types.html `SMALLINT`]] [def __MEDIUMINT__ [mysqllink integer-types.html `MEDIUMINT`]] @@ -124,47 +124,80 @@ BEGIN END ```] -[import ../../test/integration/include/test_integration/snippets/describe.hpp] -[import ../../test/integration/test/snippets/overview.cpp] -[import ../../test/integration/test/snippets/dynamic.cpp] -[import ../../test/integration/test/snippets/static.cpp] +[/ AUTOGENERATED IMPORTS BEGIN ] +[import ../../example/1_tutorial/1_sync.cpp] +[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/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] +[import ../../example/2_simple/multi_function.cpp] +[import ../../example/2_simple/callbacks.cpp] +[import ../../example/2_simple/coroutines_cpp11.cpp] +[import ../../example/2_simple/unix_socket.cpp] +[import ../../example/2_simple/batch_inserts.cpp] +[import ../../example/2_simple/batch_inserts_generic.cpp] +[import ../../example/2_simple/dynamic_filters.cpp] +[import ../../example/2_simple/patch_updates.cpp] +[import ../../example/2_simple/source_script.cpp] +[import ../../example/2_simple/pipeline.cpp] +[import ../../example/3_advanced/connection_pool/main.cpp] +[import ../../example/3_advanced/connection_pool/types.hpp] +[import ../../example/3_advanced/connection_pool/repository.hpp] +[import ../../example/3_advanced/connection_pool/repository.cpp] +[import ../../example/3_advanced/connection_pool/handle_request.hpp] +[import ../../example/3_advanced/connection_pool/handle_request.cpp] +[import ../../example/3_advanced/connection_pool/server.hpp] +[import ../../example/3_advanced/connection_pool/server.cpp] +[import ../../example/3_advanced/connection_pool/log_error.hpp] [import ../../test/integration/test/snippets/prepared_statements.cpp] -[import ../../test/integration/test/snippets/multi_resultset.cpp] +[import ../../test/integration/test/snippets/connection_establishment.cpp] +[import ../../test/integration/test/snippets/charsets.cpp] +[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/metadata.cpp] -[import ../../test/integration/test/snippets/charsets.cpp] -[import ../../test/integration/test/snippets/time_types.cpp] -[import ../../test/integration/test/snippets/any_connection.cpp] [import ../../test/integration/test/snippets/connection_pool.cpp] -[import ../../test/integration/test/snippets/sql_formatting.cpp] -[import ../../test/integration/test/snippets/sql_formatting_custom.cpp] +[import ../../test/integration/test/snippets/time_types.cpp] [import ../../test/integration/test/snippets/pipeline.cpp] +[import ../../test/integration/test/snippets/sql_formatting.cpp] +[import ../../test/integration/test/snippets/overview.cpp] +[import ../../test/integration/test/snippets/dynamic.cpp] +[import ../../test/integration/test/snippets/multi_resultset.cpp] +[import ../../test/integration/test/snippets/static.cpp] +[/ AUTOGENERATED IMPORTS END ] +[import ../../test/integration/include/test_integration/snippets/describe.hpp] [include 01_intro.qbk] [include 02_integrating.qbk] -[include 03_tutorial.qbk] +[include 03_1_tutorial_sync.qbk] +[include 03_2_tutorial_async.qbk] +[include 03_3_tutorial_with_params.qbk] +[include 03_4_tutorial_static_interface.qbk] [include 04_overview.qbk] -[include 05_dynamic_interface.qbk] -[include 06_static_interface.qbk] -[include 07_queries.qbk] -[include 08_prepared_statements.qbk] -[include 09_multi_resultset.qbk] -[include 10_multi_function.qbk] -[include 11_metadata.qbk] -[include 12_async.qbk] -[include 13_ssl.qbk] -[include 14_other_streams.qbk] -[include 15_error_handling.qbk] -[include 16_connparams.qbk] -[include 17_reconnecting.qbk] -[include 18_charsets.qbk] -[include 19_time_types.qbk] -[include 20_any_connection.qbk] -[include 21_connection_pool.qbk] -[include 22_sql_formatting.qbk] -[include 23_sql_formatting_advanced.qbk] -[include 24_pipeline.qbk] -[include 24_examples.qbk] +[include 05_connection_establishment.qbk] +[include 06_sql_formatting.qbk] +[include 07_prepared_statements.qbk] +[include 08_dynamic_interface.qbk] +[include 09_static_interface.qbk] +[include 10_multi_resultset.qbk] +[include 11_multi_function.qbk] +[include 12_connection_pool.qbk] +[include 13_async.qbk] +[include 14_error_handling.qbk] +[include 15_sql_formatting_advanced.qbk] +[include 16_metadata.qbk] +[include 17_charsets.qbk] +[include 18_time_types.qbk] +[/ TODO: re-enable this +[include 19_templated_connection.qbk] ] +[include 20_pipeline.qbk] +[include 21_examples.qbk] + [section:ref Reference] diff --git a/doc/qbk/02_integrating.qbk b/doc/qbk/02_integrating.qbk index 27bbd9211..ece05633f 100644 --- a/doc/qbk/02_integrating.qbk +++ b/doc/qbk/02_integrating.qbk @@ -46,7 +46,7 @@ target_link_libraries(main PRIVATE Boost::charconv Threads::Threads OpenSSL::Cry an older version, use the `Boost::headers` target, instead. ] -If you're happy with header-only mode, have a look at [link mysql.tutorial the tutorial] +If you're happy with header-only mode, have a look at [link mysql.tutorial_sync the first tutorial] or [link mysql.examples any of the examples] to learn how to use the library. [endsect] diff --git a/doc/qbk/03_1_tutorial_sync.qbk b/doc/qbk/03_1_tutorial_sync.qbk new file mode 100644 index 000000000..d217a3b03 --- /dev/null +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -0,0 +1,119 @@ +[/ + 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_sync Tutorial 1: hello world!] + +In this first tutorial, we will write a simple program +to demonstrate the basic concepts. We will connect to the server +and issue the query `SELECT "Hello World!"`. + +To run this tutorial, you need a running MySQL server listening +in localhost on port 3306 (the default one). You should have +the credentials of a valid MySQL user (username and password). +No further setup is needed. + +The full program listing for this tutorial can be found [link mysql.examples.tutorial_sync here]. + +We will follow these steps: + +# Create a connection object. +# Establish a session with the server. +# Issue the query. +# Use the rows generated by the query. +# Close the connection. + +This tutorial uses synchronous functions with exceptions, +as they're easy to use. In subsequent tutorials, we will +learn how to use asynchronous functions, which are more versatile. + +[heading Namespace conventions] + +All functions and classes reside within the `boost::mysql` namespace. +To reduce verbosity, all examples and code samples use the following namespace aliases: + +[tutorial_sync_namespaces] + + + + +[heading Connection object] + +Like most Asio-based applications, we need to create a +[asioreflink io_context io_context] object before anything else. +An `io_context` is an execution context: it contains an event loop, +file descriptor states, timers and other items required to perform I/O. +Most applications should only create a single `io_context`, even when +multiple MySQL connections are needed. + +We then create an [reflink any_connection], which represents a single connection +to a MySQL server: + +[tutorial_sync_connection] + + + + +[heading Connecting to the server] + +[refmem any_connection connect] establishes a client session with the server. +It takes a [reflink connect_params] object with the required +information to establish a session: + +[tutorial_sync_connect] + + + + +[heading Issuing the SQL query] + +[refmem any_connection execute] accepts a string containing +the SQL query to run and sends it to the server for execution. +It returns a [reflink results] object, containing the rows returned by the query: + +[tutorial_sync_query] + + + + +[heading Obtaining the results] + +[reflink results] is a class that holds the result of a query in memory. +To obtain the value we selected, we can write: + +[tutorial_sync_results] + +Let's break this into steps: + +* [refmem results rows] returns all the rows that this object contains. + It returns a [reflink rows_view], which is a 2D matrix-like structure. +* `result.rows().at(0)` returns the first row, represented as a [reflink row_view]. +* `result.rows().at(0).at(0)` returns the first field in the first row. This is a + [reflink field_view], a variant-like class that can hold any type allowed in MySQL. +* The obtained `field_view` is streamed to `std::cout`. + + + + +[heading Closing the connection] + +Once we are done with the connection, we can close it by calling +[refmem any_connection close]. Note that +this will send a final quit packet to the MySQL server to notify +we are closing the connection, and thus may fail. + +[tutorial_sync_close] + + + + +[heading Next steps] + +Full program listing for this tutorial is [link mysql.examples.tutorial_sync here]. + +You can now proceed to [link mysql.tutorial_async the next tutorial]. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/03_2_tutorial_async.qbk b/doc/qbk/03_2_tutorial_async.qbk new file mode 100644 index 000000000..a7f6d11d4 --- /dev/null +++ b/doc/qbk/03_2_tutorial_async.qbk @@ -0,0 +1,105 @@ +[/ + 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_async Tutorial 2: going async with C++20 coroutines] + +In the [link mysql.tutorial_sync previous tutorial] we used +synchronous functions. They are simple, but have a number of limitations: + +* They aren't as versatile as async functions. For example, there is no way + to set a timeout to a sync operation. +* They don't scale well. Since sync functions block the calling thread until they complete, + you need to create OS threads to achieve parallelism. This doesn't scale well + and leads to the inherent complexities of multi-threaded programs. +* Some classes (like [reflink connection_pool]) only offer an async interface. + +For this reason, we recommend to always use async functions. +All Asio-compatible libraries (including this one) allow async +programming using a variety of styles. In this chapter, we will +explain how to use C++20 coroutines because they are the simplest to use. + +[note + Still not using C++20? Don't worry, you can use + [link mysql.examples.coroutines_cpp11 stackful coroutines] and + [link mysql.examples.callbacks callbacks] even in C++11. +] + + + +[heading What is a coroutine?] + +Roughly speaking, it's a function that can suspend and resume, keeping local variables +alive in the process. Suspension happens when reaching a `co_await` expression. +These usually appear when the program performs an I/O operation. +When an expression like this is encountered, the following happens: + +# The coroutine initiates the I/O operation. +# The coroutine suspends, passing control back to the `io_context` (that is, the event loop). +# While the I/O operation is in progress, the `io_context` may run other operations, + like other coroutines. +# When the I/O operation completes, the `io_context` resumes the coroutine + immediately after the `co_await` expression. + + + + + +[heading Transforming sync code into coroutines] + +Recall the following code from our previous tutorial: + +[tutorial_sync_main] + +To transform this code into a coroutine, we need to: + +* Extract it to a separate function returning `boost::asio::awaitable`. +* Replace sync functions (like [refmem any_connection connect]) by async ones + (like [refmem any_connection async_connect]). +* Place a `co_await` operator in front of each I/O operation. + +Doing this, we have: + +[tutorial_async_coro] + +Note that the coroutine doesn't create or return explicitly any +`boost::asio::awaitable` object - this is handled by the compiler. +The return type actually marks the function as being a coroutine. +`void` here means that the coroutine doesn't return anything. + +If any of the above I/O operations fail, an exception is thrown. +You can prevent this by [link mysql.overview.errors using `asio::redirect_error`]. + + + + +[heading Running our coroutine] + +As in the previous tutorial, we first need to create an `io_context` and a connection: + +[tutorial_async_connection] + +To run a coroutine, use [asioreflink co_spawn co_spawn]: + +[tutorial_async_co_spawn] + +Note that this will only schedule the coroutine. To actually run +it, we need to call `io_context::run`. This will block the calling +thread until all the scheduled coroutines and I/O operations complete: + +[tutorial_async_run] + + + + + +[heading Next steps] + +Full program listing for this tutorial is [link mysql.examples.tutorial_async here]. + +You can now proceed to [link mysql.tutorial_with_params the next tutorial]. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/03_3_tutorial_with_params.qbk b/doc/qbk/03_3_tutorial_with_params.qbk new file mode 100644 index 000000000..7a0d84310 --- /dev/null +++ b/doc/qbk/03_3_tutorial_with_params.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_with_params Tutorial 3: queries with parameters] + +Until now, our SQL queries were hard-coded string literals. +However, most real-world use cases involve +running queries containing user-supplied parameters. + +In this tutorial, we will be using an employee database. +You can obtain this sample database by sourcing the `example/db_setup.sql` +file in Boost.MySQL source code repository. + +The employee table is defined as: + +[!teletype] +``` + CREATE TABLE employee( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + ... -- other fields not relevant for us + ); +``` + +We will write a program that retrieves an employee by ID +and prints their full name. The employee ID will be supplied +by the user as a command line argument. In more realistic +examples, you may get it from a file or HTTP request. + + + + +[heading Avoiding SQL injection] + +We need to build a query like the following: + +[!teletype] +``` + SELECT first_name, last_name FROM employee WHERE id = +``` + +Replacing `` by the value passed by the user. + +Since we don't control the employee ID, we must consider it [*untrusted]. +We must [*never use raw string concatenation] to build queries. +Otherwise, malicious values can cause SQL injection vulnerabilities, +which are extremely severe. + +Boost.MySQL offers two options to deal with this: + +# Compose the query dynamically in the client, using specialized tools + to avoid SQL injection. This option is versatile, simple and + appropriate for general use. +# Perform parameter substitution server side using + [link mysql.prepared_statements prepared statements]. This is more complex + and better suited for cases involving lots of numeric data or + executing same query repeatedly. + +In this tutorial, we will use client-side generation +(termed [link mysql.sql_formatting client-side SQL formatting] throughout the documentation). + + + + +[heading Using with_params] + +[refmem any_connection async_execute] +can also deal with queries with parameters. +We need to replace the string literal by a call +to [reflink with_params], passing a query template +and the actual values of the parameters: + +[tutorial_with_params_execute] + +The query template uses a syntax similar to `std::format`. +You can use numbers, strings, +dates, times and many other types as parameters. +More information about client-side SQL formatting +is available in [link mysql.sql_formatting this page]. + + + +[heading Using the retrieved rows] + +Our query might return either one row (if an employee is found) +or none (if no employee with the given ID exists). +Accounting for this: + +[tutorial_with_params_results] + + + + + +[heading Connecting with database] + +If you've run `example/db_setup.sql`, the `employee` table +exists within the `boost_mysql_examples` database. +To use this table without qualification, we need to specify +a database name when connecting. This is achieved by +setting [refmem connect_params database]: + +[tutorial_with_params_connect_params] + + + + + +[heading Creating the connection inside the coroutine] + +Since we're connecting and closing the connection in our coroutine, it +makes sense to make it a local variable, instead of passing it as parameter. +Recall that we need a reference to an execution context (i.e. `io_context`) +to build a connection. We could pass the `io_context` as a parameter +to our coroutine, but there's a simpler way: coroutines +already hold a reference to where they are executing: + +[tutorial_with_params_connection] + +The expression `co_await asio::this_coro::executor` retrieves the [*executor] +that our coroutine is using. An executor is a lightweight handle +to an execution context, and can be used to create our connection. + +[note + `co_await asio::this_coro::executor` does not perform any I/O. + It only retrieves the current coroutine's executor. +] + + + + + +[heading Wrapping up] + +With all these changes, this is how our coroutine looks like: + +[tutorial_with_params_coroutine] + +Full program listing for this tutorial is [link mysql.examples.tutorial_with_params here]. + +You can now proceed to [link mysql.tutorial_static_interface the next tutorial]. + + +[endsect] \ No newline at end of file diff --git a/doc/qbk/03_4_tutorial_static_interface.qbk b/doc/qbk/03_4_tutorial_static_interface.qbk new file mode 100644 index 000000000..89245d629 --- /dev/null +++ b/doc/qbk/03_4_tutorial_static_interface.qbk @@ -0,0 +1,108 @@ +[/ + 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_static_interface Tutorial 4: the static interface] + +Until now we've read the rows generated by our queries into +[reflink results] objects. As we've seen, `results` +is a 2D structure that contains variant-like values. +Throughout the documentation, these variant-based APIs +are called [link mysql.dynamic_interface the dynamic interface]. + + + +[heading Dynamic interface limitations] + +Working with variant-like objects can be cumbersome. +Recall the following lines from our previous tutorial, +where we used the retrieved rows: + +[tutorial_with_params_results] + +An employee is represented here by a [reflink row_view], which is a collection +of [reflink field_view] objects. `field_view` is a variant-like type +that can represent all the types supported by MySQL. + +Since `field_view` supports streaming, this code doesn't require +any casting. However, consider refactoring our code to split +the printing logic to a separate function: + +[tutorial_static_fn] + +Looking at our database schema, we know that both values are +strings. We can use [refmem field_view as_string] to perform the casts: + +[tutorial_static_casts] + +While this code works, it can create maintenance problems: + +* We retrieve fields by position. That is, we know that `employee.at(0)` + holds the employee's `first_name`. + However, if we refactor our query, we might forget updating the indices. +* Following a similar reasoning, the casts are error-prone. +* Both `at` and `as_string` throw on error. Attempting to avoid exceptions + while still performing the required safety checks quickly becomes unmanageable. + +If we know the types returned by our queries at compile time, +we can use the static interface, instead. This interface can +parse the rows returned by a query into instances of +a C++ struct defined by us. + +[note + The static interface requires C++14 or later. +] + + + + +[heading Using static_results with Boost.Pfr] + +In C++20 and later, we can use __Pfr__ and the [reflink static_results] +class to parse the rows. The first step is to define a plain C++ +struct with the fields we expect in our query: + +[tutorial_static_struct] + +We now replace the [reflink results] object by a [reflink static_results]. +The marker type [reflink pfr_by_name] indicates Boot.MySQL that it should +use Boost.Pfr for reflection. + +[tutorial_static_execute] + +Using the retrieved data is now much easier, since [refmem static_results rows] +returns a `span`: + +[tutorial_static_results] + +When using the static interface, `async_execute` will perform a set of +checks on the query results to ensure compatibility between the +C++ types and what MySQL returns. These checks aim to discover +potential problems as early as possible, and are called +[link mysql.static_interface.meta_checks metadata checks]. + + + +[heading Using the static interface in C++14] + +C++20 makes it possible to use Boost.Pfr as described here, +which is the easiest option. Boost.MySQL also supports __Describe__ +and `std::tuple`'s, which can be used in C++14. +The mechanics are quite similar to what's been explained here. + + + +[heading Wrapping up] + +[link mysql.static_interface This section] contains more information about the static interface. + +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. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/03_tutorial.qbk b/doc/qbk/03_tutorial.qbk deleted file mode 100644 index 218c0dc78..000000000 --- a/doc/qbk/03_tutorial.qbk +++ /dev/null @@ -1,109 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:tutorial Tutorial] - -[import ../../example/tutorial.cpp] - -Welcome to Boost.MySQL's tutorial. We will go through the simplest -possible piece of code using Boost.MySQL: a program that connects -to the MySQL server and issues the query `SELECT "Hello World!"`. - -To run this tutorial, you need a running MySQL server listening -in localhost on port 3306 (the default one). You should have -the credentials of a valid MySQL user (username and password). -No further setup is needed. - -This tutorial assumes you have a basic familiarity with __Asio__ -(e.g. you know what a [asioreflink io_context io_context] is). - -You can find the full source code for this tutorial [link mysql.tutorial.listing here]. - -[heading Connection object] - -The first step is to create a connection object, which -represents a single connection over TCP to the MySQL server. -We will connect to the server using TCP over TLS, using port 3306, so -we will use [reflink tcp_ssl_connection]. -If you're using the latest MySQL version with its default configuration, -you will need to use TLS to successfully establish a connection. - -A [reflink tcp_ssl_connection] is an I/O object. It can be constructed from a -[asioreflink io_context/executor_type io_context::executor_type] and a -[asioreflink ssl__context ssl::context]: - -[tutorial_connection] - -[heading Connecting to the server] - -The next step is to connect to the server. We will use the function -[reflink2 connection.connect tcp_ssl_connection::connect], -which accepts two parameters: - -* The first one specifies the network address of the MySQL server. - As we are using TCP, this is a [asioreflink ip__tcp/endpoint ip::tcp::endpoint], - which holds an IP address and a port. We will use a `boost::asio::ip::tcp::resolver` - to resolve the hostname into an IP address and thus obtain a `boost::asio::ip::tcp::endpoint`. -* The second one is an instance of [reflink handshake_params], which holds per-connection settings, - including the username and password to use. - -[tutorial_connect] - -[note - Read-only strings, like the ones used in [reflink handshake_params]'s - constructor, are represented as [reflink string_view]'s, which are similar - to `std::string_view`'s but do not require C++17 to work. -] - -[heading Issuing the SQL query] - -The next step is to issue the query to the server. We will use -[reflink2 connection.execute tcp_ssl_connection::execute], -which accepts a string containing a single SQL query and instructs -the server to run it. It returns a [reflink results] -object, containing the rows returned by the query: - -[tutorial_query] - -[heading Obtaining the results] - -[reflink results] is a class that holds the result of a query in memory. -To obtain the value we selected, we can write: - -[tutorial_results] - -Let's break this into steps: - -* [refmem results rows] returns all the rows that this object contains. - It returns a [reflink rows_view], which is a matrix-like structure. -* `result.rows().at(0)` returns the first row, represented as a [reflink row_view]. -* `result.rows().at(0).at(0)` returns the first field in the first row. This is a - [reflink field_view], a variant-like class that can hold any type allowed in MySQL. -* The obtained `field_view` is streamed to `std::cout`. - -[heading Closing the connection] - -Once we are done with the connection, we close it by calling -[reflink2 connection.close tcp_ssl_connection::close]. Note that -this will send a final quit packet to the MySQL server to notify -we are closing the connection, and thus may fail. - -[tutorial_close] - -[heading Final notes] - -This concludes our tutorial! You can now learn more about the -core functionality of this library in the [link mysql.overview overview section]. -You can also look at more complex [link mysql.examples examples]. - -[heading:listing Full listing] - -Here is the full source code for the above steps: - -[tutorial_listing] - -[endsect] \ No newline at end of file diff --git a/doc/qbk/04_overview.qbk b/doc/qbk/04_overview.qbk index 646d1d069..c83a706ab 100644 --- a/doc/qbk/04_overview.qbk +++ b/doc/qbk/04_overview.qbk @@ -11,105 +11,150 @@ This section briefly explains the library main classes and functions, and how to use them. -[section:connection Connection objects] - -[reflink connection] is the most important class in the library. A connection is an I/O object, templated on -a [reflink Stream] type. A `connection` contains an instance of that `Stream` type and additional state required -by the protocol. `connection`'s constructor takes the same arguments as the underlying `Stream` constructor. +Boost.MySQL exposes sync and async functions implementing functionality involving I/O. +As explained [link mysql.tutorial_async in the second tutorial], +it's advisable to use async functions when possible, because they are more flexible. + +Boost.MySQL supports the Boost.Asio universal async model. This means that +a variety of async programming paradigms can be used with the library, +including callbacks, stackful coroutines and C++20 coroutines. +We will use C++20 coroutines throughout the document because they're +easy to use. + +[note + Still not using C++20? Don't worry, you can use + [link mysql.examples.coroutines_cpp11 stackful coroutines] and + [link mysql.examples.callbacks callbacks] even in C++11. +] -The library defines some typedefs to make things less verbose. The most common one is [reflink tcp_ssl_connection]. -In this case, `Stream` is `boost::asio::ssl::stream`, -which can be constructed from a [asioreflink any_io_executor any_io_executor] -and a [asioreflink ssl__context ssl::context]: -[overview_connection] -Typedefs for other transports are also available. See [link mysql.other_streams this section] for more info. -[endsect] [section Connection establishment] -The MySQL client/server protocol is session-oriented. Before anything else, you must perform session -establishment, usually by calling [refmem connection connect]. This function performs two actions: +[reflink any_connection] is the most primitive I/O object in the library. +It can establish and close connections, run queries and manage prepared statements. +Like most I/O objects, `any_connection` can be constructed from an execution context: -* It establishes the "physical" connection, by calling `connect()` on the underlying `Stream` - object. For a [reflink tcp_ssl_connection], this establishes the TCP connection. -* It performs the handshake with the MySQL server. This is part of the MySQL client/server - protocol. It performs authentication, sets session parameters like the default database - to use, and performs the TLS handshake, if required. +[tutorial_sync_connection] -[refmem connection connect] takes two parameters, one for each of the above actions: +`any_connection` is named like this for historic reasons: +a templated connection class came before it. +We currently recommend using `any_connection` for new +code because it's simpler and more powerful. -* The physical endpoint where the server is listening. For TCP streams, this is a - `boost::asio::ip::tcp::endpoint`. For UNIX sockets, it's a `boost::asio::local::stream_protocol::endpoint`. - For TCP, we can resolve a string hostname and port into an endpoint using a `resolver` object. -* [reflink handshake_params] to use for the handshake operation. This parameter doesn't depend on the `Stream` - type. See [link mysql.connparams this section] for more info. +The MySQL client/server protocol is session-oriented. Before anything else, +you must perform session establishment by calling [refmem any_connection async_connect]: [overview_connect] -Note that [refmem connection connect] can only be used with socket-like streams. If your stream -is not a socket, you must use the lower-level [refmem connection handshake] function. Please -read [link mysql.other_streams.non_sockets this section] for more info. +[refmemunq any_connection async_connect] performs the hostname resolution, +TCP session establishment, TLS handshake and MySQL handshake. +By default, TLS is used if the server supports it. + +You can configure a number of parameters here, including +the database to use, TLS options and buffer sizes. +See [link mysql.connection_establishment this section] for more info. + +Boost.MySQL also supports +[link mysql.connection_establishment.unix using UNIX-domain sockets]. + +To cleanly terminate a connection, use [refmemunq any_connection async_close]. +This sends a packet informing of the imminent close and shuts down TLS. +The connection destructor will also close the socket, so no leak occurs. [endsect] -[section:queries_stmts Text queries and prepared statements] -The two main ways to use a connection are text queries and prepared statements. -You can access both using [refmem connection execute]: + + + +[section Running queries] + +The simplest way to run a SQL query is using [refmem any_connection async_execute]. +You can execute queries by passing a string as first parameter: + +[overview_text_query] + +Most queries contain user-supplied input. [*Never use raw string concatenation] +to build queries, since this is vulnerable to SQL injection. +Boost.MySQL provides two interfaces to run queries with parameters: + [table [ [Feature] - [Used for...] [Code] ] [ [ - Text queries - ] - [ - Simple queries, without parameters: + [link mysql.sql_formatting Client-side SQL formatting]: - * `"START TRANSACTION"` - * `"SET NAMES utf8"` - * `"SHOW TABLES"` - + * Securely expands queries client-side. + * Text-based protocol. + * Adequate for general use. ] [ - [overview_query_use_case] + [overview_with_params] ] ] [ [ - Prepared statements - ] - [ - Queries with parameters unknown at compile-time. + [link mysql.prepared_statements Prepared statements]: + + * Parsed and executed in two different operations. + * Binary protocol. + * Adequate when running a query several times or retrieving + lots of numeric data. ] [ - [overview_statement_use_case] + [overview_statement] ] ] ] +By default, we recommend using [reflink with_params] because it's +simpler and entails less round-trips to the server. +See [link mysql.sql_formatting.comparison the comparison section] for more info. + +Client-side SQL formatting can also be used to +[link mysql.sql_formatting_advanced.expand expand queries] +without sending them to the server. + [endsect] + + + + [section The dynamic and the static interfaces] -There are two different interfaces to access the rows generated by a query or statement. +In MySQL, a ['resultset] refers to the results generated by a SQL query. +A resultset is composed of rows, [link mysql.meta metadata] and +additional info, like the last insert ID. + +There are two different interfaces to access resultsets. You can use the [reflink results] class to access rows using a dynamically-typed interface, -using variant-like objects to represent values retrieved from the server. On ther other hand, +using variant-like objects to represent values retrieved from the server. On other other hand, [reflink static_results] is statically-typed. You specify the shape of your rows at compile-time, and the library will parse the retrieved values for you into the types you provide. You can use almost every feature in this library (including text queries and prepared statements) with both interfaces. -For example, given the following table: +For example, given the following table : -[overview_ifaces_table] +[/ (TODO: this code is duplicated.) ] + +[!teletype] +``` + CREATE TABLE employee( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + ... -- other fields not relevant for us + ); +``` This is how you would access its contents using either of the interfaces: @@ -124,10 +169,9 @@ This is how you would access its contents using either of the interfaces: Dynamic interface: [reflink results] ] [ - Variant based[br] - Available in C++11[br] - [link mysql.dynamic_interface Learn more][br] - [link mysql.examples.prepared_statements_cpp11 Example code] + * Variant based + * Available in C++11 + * [link mysql.dynamic_interface Learn more] ] [ [overview_ifaces_dynamic] @@ -138,141 +182,99 @@ This is how you would access its contents using either of the interfaces: Static interface: [reflink static_results] ] [ - Parses rows into your own types[br] - Requires C++14[br] - [link mysql.static_interface Learn more][br] - [link mysql.examples.prepared_statements_cpp14 Example code] + * Parses rows into your own types + * Requires C++20 when using Boost.Pfr, C++14 when using Boost.Describe + * [link mysql.static_interface Learn more] ] [ - [describe_post][br] + [overview_static_struct][br] [overview_ifaces_static] ] ] ] +Prefer using the static interface when possible. + [endsect] -[section Resultsets] -In MySQL, a ['resultset] referes to the results generated by a SQL query. When you execute a text query -or a prepared statement, you get back a resultset. We've already been using resultsets: the [reflink results] -and [reflink static_results] classes are in-memory representations of a resultset. -A resultset is composed of three pieces of data: -[variablelist - [ - [Rows] - [ - The actual rows generated by the SQL query: [refmem results rows] and [refmem static_results rows]. - ] - ] - [ - [Metadata] - [ - Information about the columns retrieved by the query: [refmem results meta] and [refmem static_results meta]. - There is one object per retrieved column. It provides information about column names, types, uniqueness contraints... - ] - ] - [ - [Additional execution information] - [ - Extra info on the execution of the operation, like the number of affected rows ([refmem results affected_rows] - and [refmem static_results affected_rows]) or the last auto-generated ID for `INSERT` statements ([refmem results last_insert_id] - and [refmem static_results last_insert_id]). - ] - ] -] -All SQL statements generate resultsets. Statements that generate no rows, like `INSERT`s, generate empty resultsets -(i.e. `result.rows().empty() == true`). The interface to execute `SELECT`s and `INSERT`s is the same. - -[endsect] -[section:statements Prepared statements] +[section Running INSERT, UPDATE and DELETE statements] -Until now, we've used simple text queries that did not contain any user-provided input. -In the real world, most queries will contain some piece of user-provided input. +The same APIs explained above can be used for SQL statements +that don't retrieve data: -One approach could be to use string concatenation to construct a SQL query from user input, -and then pass it to `execute()`. Avoid this approach as much as possible, as it can lead -to [*SQL injection vulnerabilities]. Instead, [*use prepared statements]. +[overview_update] -Prepared statements are server-side objects that represent a parameterized query. A statement is -represented using the [reflink statement] class, which is a lightweight object holding a -handle to the server-side prepared statement. +When performing INSERTs, you might find [refmem results last_insert_id] +handy, which retrieves the last AUTO INCREMENT ID generated by the executed statement. -Let's say you've got an inventory table, and you're writing a command-line program to get products -by ID. You've got the following table definition: +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. -[overview_statements_setup] +[endsect] -You can prepare a statement to retrieve products by ID using [refmem connection prepare_statement]: -[overview_statements_prepare] -You can execute the statement using [refmem connection execute]: -[overview_statements_execute] -We used [refmem statement bind] to provide actual parameters to the statement. -You must pass as many parameters to `bind` as `?` placeholders the statement has. +[section:async Single outstanding async operation per connection] -To learn more about prepared statements, please refer to [link mysql.prepared_statements this section]. +At any given point in time, a `any_connection` may only have a single async operation outstanding. +Because MySQL sessions are stateful, and to keep the implementation simple, messages +are written to the underlying transport without any locking or queueing. +If you perform several async operations concurrently on a single connection without any +serialization, messages from different operations will be interleaved, leading to undefined behavior. -[endsect] +For example, doing the following is illegal and should be avoided: +[overview_async_dont] -[section:errors Error handling] +If you need to perform queries in parallel, open more connections to the server. -The functions we've been using communicate errors throwing exceptions. There are also non-throwing -overloads that use error codes. +[endsect] -If the server fails to fulfill a request (for example, because the provided SQL was invalid or a -referenced table didn't exist), the operation is considered failed and will return an error. -The server provides an error message that -can be accessed using the [reflink diagnostics] class. For example: -[overview_errors_sync_errc] -With exceptions, this would be: -[overview_errors_sync_exc] -[endsect] -[section:async Asynchronous functions] +[section:errors Error handling] -As with Boost.Asio, every sync operation has an async counterpart. This library follows -Asio's async model, so you may use async operations with any valid Asio `CompletionToken`, including callbacks -and coroutines. +An operation fails if a network error happens, +a protocol violation is encountered, or the server reports an error. +For instance, SQL syntax errors make `async_execute` fail. -For example, if you can use C++20, you can write: +When the server reports an error, it provides a diagnostic string +describing what happened. The [reflink diagnostics] class encapsulates +this message. Some library functions generate diagnostics strings, too. -[overview_async_coroutinescpp20] +Both the sync functions in [link mysql.tutorial_sync the first tutorial] +and the coroutines in this exposition throw exceptions when they fail. +The exception type is [reflink error_with_diagnostics], which inherits +from `boost::system::system_error` and adds a [reflink diagnostics] object. +Async functions use [reflink with_diagnostics], a completion token adapter, +to transparently include diagnostics in exceptions. -The [link mysql.examples examples section] contains material that can help you. -[link mysql.async This section] also provides more info on this topic. +You can avoid exceptions when using coroutines with `asio::redirect_error`: -[h4 Single outstanding operation per connection] +[overview_no_exceptions] -At any given point in time, a `connection` may only have a single async operation outstanding. -This is because the connection uses the underlying `Stream` object directly, without any locking -or queueing. If you perform several async operations concurrently on a single connection without any -serialization, the stream may interleave reads and writes from different operations, leading to undefined behavior. +[reflink2 error_code mysql::error_code] is an alias for `boost::system::error_code`. -For example, doing the following is illegal and should be avoided: +[endsect] -[overview_async_dont] -If you need to perform queries in parallel, open more connections to the server. -[endsect] [section Multi-function operations] -Until now, we've been using [refmem connection execute], which +Until now, we've been using [refmemunq any_connection async_execute], which executes some SQL and reads all generated data into an in-memory object. Some use cases may not fit in this simple pattern. For example: @@ -282,14 +284,14 @@ Some use cases may not fit in this simple pattern. For example: * If rows contain very long `TEXT` or `BLOB` fields, it may not be adequate to copy these values from the network buffer into the `results` object. A view-based approach may be better. -For these cases, we can break the `execute()` operation into several steps, +For these cases, we can break the execute operation into several steps, using a ['multi-function operation] (the term is coined by this library). This example reads an entire table in batches, which can be the case in an ETL process: [overview_multifn] [warning - Once you start a multi-function operation with [refmem connection start_execution], + Once you start a multi-function operation with [refmemunq any_connection async_start_execution], the server immediately sends all rows to the client. [*You must read all rows] before engaging in further operations. Otherwise, you will encounter packet mismatches, which can lead to bugs and vulnerabilities! ] @@ -301,4 +303,32 @@ Please refer to [link mysql.multi_function this section] for more information on [endsect] + +[section Connection pools] + +Connection pooling is a technique where several long-lived connections +are re-used for independent logical operations. When compared to +establishing individual connections, it has the following benefits: + +* It provides better performance. Please consult [link mysql.connection_pool.benchmarks our benchmarks] + for more info. +* It simplifies connection management. The connection pool will establish sessions, + perform retries and apply timeouts out of the box. + +This is how you can create a pool of connections: + +[connection_pool_create] + +[refmem connection_pool async_run] must be called exactly once per pool. +This function takes care of actually keeping connections healthy. + +To retrieve a connection, use [refmem connection_pool async_get_connection]: + +[connection_pool_get_connection] + +For more info, see [link mysql.connection_pool this section]. + +[endsect] + + [endsect] diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk new file mode 100644 index 000000000..8436b8c58 --- /dev/null +++ b/doc/qbk/05_connection_establishment.qbk @@ -0,0 +1,294 @@ +[/ + 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:connection_establishment Connection establishment and termination] +[nochunk] + +This section discusses several aspects regarding the creation, +establishment and termination of client connections. + + + + + +[section Authentication] + +[refmem connect_params username] and [refmem connect_params password] +contain the credentials used during authentication. +The password is sent to the server either hashed or over a secure +channel such as TLS, as mandated by the protocol. + +MySQL implements several authentication plugins that can be used +to authenticate a user (see the [mysqllink pluggable-authentication.html +pluggable authentication MySQL docs]). +Each MySQL user is associated to a single authentication +plugin, specified during using creation. +Additionally, servers define a default authentication plugin +(see [mysqllink server-system-variables.html#sysvar_authentication_policy `authentication_policy`] and +[mysqllink server-system-variables.html#sysvar_default_authentication_plugin +`default_authentication_plugin`]). The default plugin will +be used for newly created users, and may affect how the handshake works. + +This library implements the two most common authentication plugins: + +* [mysqllink native-pluggable-authentication.html `mysql_native_password`]. + Unless otherwise configured, this is the default plugin for + MySQL 5.7 and MariaDB. It can be used over both TLS and plaintext + connections. It sends the password hashed, salted by a nonce. +* [mysqllink caching-sha2-pluggable-authentication.html + `caching_sha2_password`]. This is the default plugin for + MySQL 8.0+. It can only be used over secure transports, + like TCP with TLS or UNIX sockets. + + +Multi-factor authentication is not yet supported. +If you require support for a plugin not listed above or for MFA, +please file a feature request against the GitHub repository. + +[note + Servers configured with a default authentication plugin + not implemented in Boost.MySQL are not supported, regardless + of the actual plugin the concrete user employs. This limitation + may be lifted in the future. +] + +[endsect] + + + +[section Connect with database] + +[refmem connect_params database] contains the database name +to connect to. If you specify it, your connection will default to +use that database, as if you had issued a __USE__ statement. +You can leave it blank to select no database. +You can always issue a __USE__ statement using [refmemunq any_connection async_execute] +to select a different database after establishing the connection. + +[endsect] + + + + +[section:tls TLS support] + +TLS encrypted connections are fully supported by Boost.MySQL. +TCP connections established using [reflink any_connection] use TLS by default. + +[heading TLS handshake and termination] + +The TLS handshake is performed by [refmem any_connection async_connect]. +This contrasts with libraries like __Beast__, where the TLS handshake +must be explicitly invoked by the user. +We selected this approach because the TLS handshake is part of the MySQL protocol's handshake: +the client and server exchange several unencrypted messages, then perform the TLS handshake +and continue exchanging encrypted messages, until the connection either succeeds or fails. +This scheme enables the TLS negotiation feature (see below for more info). + +If the TLS handshake fails, the entire [refmemunq any_connection async_connect] +operation will also fail. + +TLS shutdown is performed by [refmem any_connection async_close]. +MySQL doesn't always close TLS connections +gracefully, so errors generated by the TLS shutdown are ignored. + + +[heading:negotiation TLS negotiation] + +During connection establishment, client and server negotiate whether to use TLS or not. +Boost.MySQL supports such negotiation using [refmem connect_params ssl]. This is +a [reflink ssl_mode] enum with the following options: + +* `ssl_mode::enable` will make the connection use TLS if the server supports it, + falling back to a plaintext connection otherwise. [*This is the default] + for [reflink any_connection] when using TCP. +* `ssl_mode::require` ensures that the connection uses TLS. + If the server does not support it, [refmemunq any_connection async_connect] fails. +* `ssl_mode::disable` unconditionally disables TLS. + +[*UNIX sockets] are considered secure channels and [*never use TLS]. +When connecting using a UNIX socket, [refmem connect_params ssl] is ignored. + +After a successful connection establishment, you can use +[refmem any_connection uses_ssl] to query whether the connection is encrypted or not. + + +[heading Disabling TLS] + +As mentioned above, setting [refmem connect_params ssl] to `ssl_mode::disable` +disables TLS: + +[section_connection_establishment_disable_tls] + +See the [link mysql.examples.disable_tls full example here]. + + + +[heading:options Certificate validation and other TLS options] + +You can pass an optional __ssl_context__ to [reflink any_connection] +constructor. You can set many TLS parameters doing this, including +trusted CAs, certificate validation callbacks and TLS extensions. + +[reflink any_connection_params] contains a [refmemunq any_connection_params ssl_context] +member that can be used for this. For example, TLS certificate validation +is disabled by default. To enable it: + +[section_connection_establishment_tls_options] + +You can safely share a single __ssl_context__ among several connections. + +If no `ssl::context` is passed, one will be internally created by the +connection when required. The default context doesn't perform certificate validation. + +The full source code for the above example is [link mysql.examples.tls_certificate_verification here]. + + + +[heading TLS in connection_pool] + +Since [reflink connection_pool] creates [reflink any_connection] instances, +the mechanics for TLS are similar. TLS-related parameters are specified +during pool construction, as members of [reflink pool_params]: + +* [refmem pool_params ssl] controls TLS negotiation. + It's a [reflink ssl_mode] value, with the semantics explained + [link mysql.connection_establishment.tls.negotiation above]. +* [refmem pool_params ssl_ctx] is a __ssl_context__ + that is passed to connections created by the pool. + It can be used to configure + [link mysql.connection_establishment.tls.options TLS options] + like certificate verification. The pool takes ownership + of the passed `ssl::context`, as opposed to `any_connection`. + +[endsect] [/ TLS ] + + + + + + + + +[section:unix UNIX sockets] + +[refmem connect_params server_address] is an [reflink any_address], +a variant-like type that can hold a (hostname, port) pair or a UNIX socket path. +To connect to MySQL using a UNIX socket, set `server_address` +to a UNIX domain path: + +[section_connection_establishment_unix_socket] + +Note that UNIX sockets never use TLS, regardless of the value of +[refmem connect_params ssl]. + +[endsect] + + + + + +[section Changing the network buffer size limit] + +[reflink any_connection] owns an internal network buffer +used to store messages that are to be written or have been read +from the server. Its initial size is given by [refmem any_connection_params initial_buffer_size]. +Every protocol message needs to fit in memory, so the buffer is expanded as required. +When reading data, every row is sent as an individual message. + +The buffer never resizes past [refmem any_connection_params max_buffer_size]. +If an operation requires a bigger buffer, it will fail with the +`client_errc::max_buffer_size_exceeded` error code. The default size +is 64MB. + +If you need to read or write individual rows bigger than the default limit, +you can increase it when constructing the connection: + +[section_connection_establishment_max_buffer_size] + +Note that reading datasets bigger than 64MB [*does not require increasing the limit] +as long as individual rows are smaller than the aforementioned limit. + +Tweaking [refmem any_connection_params initial_buffer_size] may affect +[refmem any_connection async_read_some_rows] performance, as explained +in [link mysql.multi_function.read_some_rows this section]. + +[endsect] + + + + +[section Enabling multi-queries] + +You can run several several semicolon-separated queries at once using +[refmem any_connection async_execute]. This is a protocol feature that +is disabled by default. You can enable it by setting +[refmem connect_params multi_queries] to true before connecting: + +[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]. + +[link mysql.multi_resultset.multi_queries This section] contains more +info on how to use multi-queries. + +This protocol feature is disabled by default as a security hardening +measure. If your application contains a SQL injection vulnerability, +this feature can make exploiting it easier. Applications that don't +need this feature should leave it off as a best practice. + +[note + Using multi-queries correctly is secure. Just make sure to use + the adequate client-side SQL formatting tools to generate queries securely. +] + +[endsect] + + + +[section Closing a connection] + +You can cleanly close a connection by calling [refmem any_connection async_close]. +This sends a ['quit packet] to the server, notifying that we're about to end +the connection, performs TLS shutdown, and closes the underlying transport. +A clean close involves I/O and can thus fail. + +[*Destroying the connection] without performing a clean close +will just close the underlying transport. [*It won't leak any resource], +but you might see warnings in the server log. Try to close connections +cleanly when possible. + +[endsect] + + + +[section Reconnection and long-lived connections] + +[reflink any_connection] doesn't perform any re-connection on its own. +If a fatal error (like a network error) is encountered during an operation, +you need to re-establish the connection explicitly. + +By design, [refmem any_connection async_connect] can [*always be used] +to re-establish connections. It works even after the connection encountered +a network error or a cancellation. To achieve this, `async_connect` +will wipe any previous connection state before proceeding. + +If you need reliable, long-lived connections, consider +[link mysql.connection_pool using a connection pool] +instead of rolling out your own strategy. `connection_pool` +takes care of re-connecting and re-using connections for you. + +[endsect] + + + + +[endsect] [/ connparams] diff --git a/doc/qbk/06_sql_formatting.qbk b/doc/qbk/06_sql_formatting.qbk new file mode 100644 index 000000000..b6d6d3678 --- /dev/null +++ b/doc/qbk/06_sql_formatting.qbk @@ -0,0 +1,222 @@ +[/ + 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:sql_formatting Text queries and client-side SQL formatting] +[nochunk] + +['Text queries] are those that use MySQL text protocol for execution. +Plain strings and [reflink with_params] use this protocol. +This contrasts with [link mysql.prepared_statements prepared statements], +which are first prepared and then executed separately, and use a binary protocol. + +[warning + [*Never compose SQL queries using raw string concatenation]. + This is insecure and can lead to [*SQL injection vulnerabilities]. + Use the client-side SQL formatting facilities explained in this section + to avoid vulnerabilities. +] + +Using text queries you can run +[link mysql.multi_resultset.multi_queries multiple semicolon-separated queries], +which can improve efficiency. + + +[section Using with_params for simple queries] + +[reflink with_params] is the easiest way to use client-side SQL formatting. +It can be used as a simpler and more flexible alternative to prepared statements. +While prepared statements expand queries server-side, SQL formatting does it client-side. +Please read the [link mysql.sql_formatting.comparison comparison with prepared statements] +and the [link mysql.sql_formatting.comparison.security security considerations] sections for more info. + +[reflink with_params] takes a SQL query +string with placeholders and a set of parameters. When passed to +[refmemunq any_connection execute] or [refmemunq any_connection async_execute], +the query is expanded in the client with the supplied parameters and +sent to the server for execution: + +[sql_formatting_simple] + +Curly braces (`{}`) represent placeholders (technically called ['replacement fields]). +The notation and semantics are similar to [@https://en.cppreference.com/w/cpp/utility/format/format `std::format`]. + +All fundamental types can be used as query parameters. This includes integers, floating point types, +strings, blobs, dates and times: + +[sql_formatting_other_scalars] + +`std::optional` and `boost::optional` can also be used: + +[sql_formatting_optionals] + +Collections and ranges are supported, as long as its elements can be formatted: + +[sql_formatting_ranges] + +See [link mysql.sql_formatting_advanced.ranges this section] for more on formatting ranges, +and [link mysql.sql_formatting_advanced.reference this table] for a reference of types +that have built-in support for SQL formatting. + +[note + Like with `std::format`, the query string passed to `with_params` must be known at + compile-time. You can skip this check using the [reflink runtime] function. +] + +Like `std::format`, you can use arguments with explicit indices: + +[sql_formatting_manual_indices] + +See [link mysql.sql_formatting_advanced.format_string_syntax this section] +for a reference on the format string syntax. + +[endsect] + + + + + + +[section:errors Common errors and how to fix them] + +Not all values can be formatted. If the library finds that formatting a certain +value can cause an ambiguity that could lead to a security problem, an error +will be issued and the query won't be sent to the server. Here are the most common errors: + +* `client_errc::invalid_encoding` + * Cause: one of your string parameters contains invalid code points. + With the default character set, this means that it contains [*invalid UTF-8]. + * Solution: all string values must be encoded according to the connection's character + set (usually UTF-8). Sanitize or reject such values. Use the [reflink blob] and [reflink blob_view] + types for values that don't represent character strings, but arbitrary binary values. +* `client_errc::unformattable_value` + * Cause: one of your parameters contains an invalid value. For instance, a `double` + contains a `NaN` or an `Inf`, unsupported by MySQL. + * Solution: reject such values, or replace them by `NULL` before passing them to client-side SQL formatting. +* `client_errc::unknown_character_set` + * Cause: your connection doesn't know the character set you're using. Knowing the character set in use + is required to generate queries securely. This situation can happen after calling [refmemunq any_connection reset_connection] + or if you used a custom [refmem connect_params connection_collation] when connecting. + * Solution: use a [reflink connection_pool] instead of manually resetting connections. If you can't, + use the default [refmemunq connect_params connection_collation] when connecting, and use + [refmemunq any_connection set_character_set] or [refmemunq any_connection async_set_character_set] + after resetting connections. + * [link mysql.charsets.tracking Learn more] about how character set tracking works. + +For example: + +[sql_formatting_invalid_encoding] + +[endsect] + + + + +[section:comparison Prepared statements vs. client-side SQL formatting] + +Although both serve a similar purpose, they are fundamentally different. Prepared statements +are parsed and expanded by the server. Client-side SQL expands the query in the client +and sends it to the server as a string. + +This means that [*client-side SQL does not understand your queries]. It just knows about how +to format MySQL types into a string without creating vulnerabilities, but otherwise treats +your queries as opaque strings. Client-side SQL yields [*greater flexibility] (you can dynamically +compose any query), while statements have more limitations. This also means that +[*you need to pay more attention to compose valid queries], specially when dealing with complex conditionals. +Logic errors may lead to exploits. Please read the +[link mysql.sql_formatting.comparison.security security considerations section] +for more info. + +Client-side SQL entails [*less round-trips to the server] than statements, and is usually more efficient +for lightweight queries. However, it uses the less compact text protocol, which may be slower for +queries retrieving a lot of data. See the +[link mysql.sql_formatting.comparison.efficiency efficiency considerations section] for more info. + +In general, [*use client-side SQL] formatting for the following cases: + +* Simple queries that don't retrieve a lot of data. Default to `with_params` and + only switch to statements if your performance measurements says so. +* Queries involving dynamic SQL that can't be achieved by statements. Typical cases include: + * Dynamic filters ([link mysql.examples.dynamic_filters example]). + * Batch inserts. Inserting rows one by one can lead to poor efficiency. + You can use client-side SQL formatting to compose a single `INSERT` that + inserts several rows at once (see [link mysql.examples.batch_inserts example 1] + and [link mysql.examples.batch_inserts_generic example 2]). + * PATCH-like updates, where the field list in an `UPDATE` must be dynamic + ([link mysql.examples.patch_updates example]). + * Queries involving dynamic identifiers, like table and field names. + * Conditional sorting. + * Pipelines consisting of several semicolon-separated queries with dynamic fields. + +On the other hand, [*prefer prepared statements] if: + +* You are executing the same query over and over. You can prepare the statement + once and execute it several times. +* Your query is retrieving a lot of data, and you have performed the relevant performance measurements. + + + +[heading:efficiency Efficiency considerations] + +Both client-side SQL formatting and prepared statements have pros and cons efficiency-wise: + +* Client-formatted SQL entails [*less round-trips to the server]. For prepared statements, you usually need + a call to prepare the statement, another one to execute it, and possibly a final one to close it. + Client-formatted SQL only requires the execution round-trip. This performance gain increases with network + latency and if you are using TLS. +* Prepared statements always entail a [*mutation of session state], while client-formatted SQL may not. + If you're using a [reflink connection_pool] with prepared statements, you can't use + [refmem pooled_connection return_without_reset], as this will leak the statement. + With client-formatted queries, reset may not be required if your SQL doesn't mutate session state. +* Client-formatted SQL queries use a usually [*less efficient text-based protocol], while prepared statements + use a more compact binary protocol. This is relevant if you're retrieving lots of data that is + slow to convert to and from text (like doubles). +* [*Prepared statements can be re-used]. If you need to execute a query several times, + prepared statements will only be parsed once. +* Client-formatted SQL allows [*more efficient patterns] than prepared statements, + like batch inserts and semicolon-separated queries. + + + + +[heading:security Security considerations] + +Both client-side SQL formatting and prepared statements [*protect against SQL injection]. +Statements do so by parsing the query with placeholders server-side, before performing parameter +substitution. Client-side SQL quotes and escapes your values to avoid injection, but +[*does not understand your queries]. + +This means that you need to [*ensure that your queries always expand to valid SQL]. +This is trivial for simple queries, but may be an issue with more complex ones, +involving ranges or dynamic identifiers. For instance, the following query may +expand to invalid SQL if the provided range is empty: + +[sql_formatting_empty_ranges] + +The risk is higher if you're building your query by pieces using [reflink format_sql_to]. + +To sum up: + +* Client-side SQL protects against SQL injection. +* Client-side SQL does not protect against logic errors. The risk is only present in complex + queries. We suggest the following advice: + * Avoid complex query generation logic as much as possible. + Use a single format string instead of `format_sql_to`, unless you have no other option. + * When using ranges, consider if the empty range would lead to valid SQL or not. + * Thoroughly test complex query generation logic. +* Client-side SQL requires knowing the connection's current character set. This usually happens + out of the box, and will lead to a [link mysql.sql_formatting.errors controlled error] + otherwise. Some recommendations: + * If in doubt, always use the default character set (`utf8mb4`). + * Never issue `SET NAMES` or `SET CHARACTER SET` statements directly - + use [refmem any_connection set_character_set] or [refmemunq any_connection async_set_character_set], instead. + * If you're using [reflink format_sql] or [reflink format_sql_to], never craft [reflink format_options] values manually. + Use [refmem any_connection format_opts], instead. + + +[endsect] + +[endsect] diff --git a/doc/qbk/08_prepared_statements.qbk b/doc/qbk/07_prepared_statements.qbk similarity index 100% rename from doc/qbk/08_prepared_statements.qbk rename to doc/qbk/07_prepared_statements.qbk diff --git a/doc/qbk/07_queries.qbk b/doc/qbk/07_queries.qbk deleted file mode 100644 index 12883d520..000000000 --- a/doc/qbk/07_queries.qbk +++ /dev/null @@ -1,53 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:queries Text queries] - -To run a text query, use any of the following functions, passing a string-like -object (convertible to [reflink string_view]) containing valid SQL as the first parameter: - -* [refmem connection execute] or [refmemunq connection async_execute]: these functions run the query and - read the generated results into memory. -* [refmem connection start_execution] and [refmemunq connection async_start_execution]: these functions - initiate a text query as a multi-function operation. - -Almost any query that may be issued in the `mysql` command line -can be executed using this method. This includes `SELECT`s, -`UPDATE`s, `INSERT`s, `DELETE`s, `CREATE TABLE`s... -In particular, you may start transactions issuing a `START TRANSACTION`, -commit them using `COMMIT` and rolling them back using `ROLLBACK`. - -[heading Use cases] - -You should generally prefer prepared statements over text queries. Text queries can be useful for simple, -non-parametrized queries: - -* `"START TRANSACTION"`, `"COMMIT"` and `"ROLLBACK"` queries, for transactions. -* `"SET NAMES utf8mb4"` and similar, to set variables for encoding, time zones and similar configuration options. -* `"CREATE TABLE ..."` and similar DDL statements. - -If you need to run parametrized SQL, involving user input, you have two options: - -* Use [link mysql.prepared_statements prepared statements] instead of text queries. -* If you don't mind using experimental features, you can use [link mysql.sql_formatting client-side SQL formatting] - to securely compose queries in the client. - -[warning - [*SQL injection warning]: if you compose queries by concatenating strings without sanitization, - your code is vulnerable to SQL injection attacks. Use prepared statements or proper formatting - functions instead! -] - - -[heading Running multiple queries at once] - -You can run several semicolon-separated queries in a single `execute()` call by enabling -the [refmem handshake_params multi_queries] option. You can find an example -[link mysql.multi_resultset.multi_queries here]. - - -[endsect] diff --git a/doc/qbk/05_dynamic_interface.qbk b/doc/qbk/08_dynamic_interface.qbk similarity index 98% rename from doc/qbk/05_dynamic_interface.qbk rename to doc/qbk/08_dynamic_interface.qbk index eca9215cb..8cabc8971 100644 --- a/doc/qbk/05_dynamic_interface.qbk +++ b/doc/qbk/08_dynamic_interface.qbk @@ -196,8 +196,8 @@ Every MySQL type is mapped to a single C++ type. The following table shows these ] No character set conversion is applied on strings. They are provided -as the server sends them. If you've issued a `"SET NAMES "` statement, -strings will be encoded according to ``. For details, see [link mysql.charsets this section]. +as the server sends them. If you've run [refmemunq any_connection async_set_character_set], +strings will be encoded according to the passed character set. For details, see [link mysql.charsets this section]. [heading The field class] diff --git a/doc/qbk/06_static_interface.qbk b/doc/qbk/09_static_interface.qbk similarity index 98% rename from doc/qbk/06_static_interface.qbk rename to doc/qbk/09_static_interface.qbk index b329d5252..882033898 100644 --- a/doc/qbk/06_static_interface.qbk +++ b/doc/qbk/09_static_interface.qbk @@ -11,9 +11,9 @@ To use the static interface, we must first define a data structure that describes the shape of our rows. We have several options: -* Use [@boost:/libs/describe/index.html Boost.Describe] to annotate a plain `struct` +* Use __Describe__ to annotate a plain `struct` with `BOOST_DESCRIBE_STRUCT` to enable reflection on it. -* Use [@boost:/libs/pfr/index.html Boost.PFR] and [reflink pfr_by_name] or [reflink pfr_by_position] +* Use __Pfr__ and [reflink pfr_by_name] or [reflink pfr_by_position] to use PFR automatic reflection capabilities. * Use `std::tuple`. @@ -59,7 +59,7 @@ You can write your query as: -[heading Metadata checking] +[heading:meta_checks Metadata checking] The static interface will try to validate as soon as possible that the provided row type is compatible with the schema returned by the server. This process is known as [*metadata checking], diff --git a/doc/qbk/09_multi_resultset.qbk b/doc/qbk/10_multi_resultset.qbk similarity index 100% rename from doc/qbk/09_multi_resultset.qbk rename to doc/qbk/10_multi_resultset.qbk diff --git a/doc/qbk/10_multi_function.qbk b/doc/qbk/11_multi_function.qbk similarity index 100% rename from doc/qbk/10_multi_function.qbk rename to doc/qbk/11_multi_function.qbk diff --git a/doc/qbk/21_connection_pool.qbk b/doc/qbk/12_connection_pool.qbk similarity index 100% rename from doc/qbk/21_connection_pool.qbk rename to doc/qbk/12_connection_pool.qbk diff --git a/doc/qbk/12_async.qbk b/doc/qbk/13_async.qbk similarity index 94% rename from doc/qbk/12_async.qbk rename to doc/qbk/13_async.qbk index 65e47b2d3..f408e7d1a 100644 --- a/doc/qbk/12_async.qbk +++ b/doc/qbk/13_async.qbk @@ -47,14 +47,14 @@ with this library. Here are some of the most common: You can combine deferred with [link mysql.async.with_diagnostics with_diagnostics] to get better error reporting. - See [*[link mysql.examples.async_coroutinescpp20 this example]] for details. + See [*[link mysql.tutorial_async the async tutorial]] for details. * [*Stackful coroutines], which you can use to get coroutine-like functionality in C++11. Access this functionality using [asioreflink spawn spawn] and [asioreflink yield_context yield_context], possibly in conjunction with [link mysql.async.with_diagnostics with_diagnostics]. You need to link against __Context__ to use these coroutines. - See [*[link mysql.examples.async_coroutines this example]] for details. + See [*[link mysql.examples.coroutines_cpp11 this example]] for details. * [*Callbacks]. You can pass in a callable (function pointer or function object) with the same signature as the handler @@ -62,7 +62,7 @@ with this library. Here are some of the most common: will be called when the operation completes. The initiating function will return `void`. - [link mysql.examples.async_callbacks This example] + [link mysql.examples.callbacks This example] demonstrates how to use async functions with callbacks. * [*Futures]. In this case, you pass in the constant @@ -79,9 +79,6 @@ with this library. Here are some of the most common: be of type `boost::system::system_error`, even if diagnostics were available. - [link mysql.examples.async_futures This example] - demonstrates using futures. - * Any other type that satisfies the __CompletionToken__ type requirements. We have listed the most common ones here, but you can craft your own and use it with this library's async operations. diff --git a/doc/qbk/13_ssl.qbk b/doc/qbk/13_ssl.qbk deleted file mode 100644 index 399520ae2..000000000 --- a/doc/qbk/13_ssl.qbk +++ /dev/null @@ -1,91 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:ssl SSL/TLS] -[nochunk] - -This library fully supports connecting to MySQL over SSL/TLS. In fact, all examples -make use of TLS connections, as TLS is required for the -[mysqllink caching-sha2-pluggable-authentication.html `caching_sha2_password`] -authentication plugin, which is the default in MySQL 8.0. - -[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]. - -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. - - - -[heading When is the SSL handshake performed?] - -The SSL handshake is performed while establishing the connection to the MySQL server, -as part of the [refmem connection handshake] and [refmem connection async_handshake]. The functions -[refmem connection connect] and [refmem connection async_connect] are implemented in terms of the former, -and thus also perform the TLS handshake. - -This approach contrasts with libraries like __Beast__, where it's the user resposibility to invoke the SSL handshake on the -underlying stream before performing any operation. - -We take this approach because the SSL handshake is part of the MySQL protocol's handshake: -the client and server exchange several unencrypted messages, then perform the SSL handshake, -and continue exchanging encrypted messages, until the connection either succeeds or fails. -This scheme allows the SSL negotiation feature (see below for more info). - -You can set any SSL/TLS parameters on the [asioreflink ssl__context ssl::context] required -to create a [reflink connection] using a SSL-enabled stream type. This context will be passed -to the stream's constructor. You can configure any setting allowed by [asioreflink ssl__context ssl::context], -including SSL certificate validation. Check [link mysql.examples.ssl this example] for an example -on this topic. - -If SSL certificate validation is enabled and fails, the [refmem connection handshake] or -[refmem connection async_handshake] operation will fail with the relevant error code. - -SSL shutdown is performed by the library, too, by [refmem connection quit] and -[refmem connection async_quit]. MySQL doesn't always close SSL connections -gracefully, so these functions ignore any errors generated by the TLS shutdown. -The functions [refmem connection close] and [refmem connection async_close] -are implemented in terms of [refmem connection quit] and -[refmem connection async_quit], and thus also perform the TLS shutdown. - - -[heading:negotiation SSL negotiation] - -During the handshake, client and server will negotiate whether to use TLS or not. For SSL -capable streams, we support using TLS conditionally. This is controlled using the [reflink ssl_mode] -parameter in [reflink handshake_params], which configure the MySQL handshake process. - -There are three possible values for this [reflink ssl_mode]: - -* If set to `require`, the connection will use TLS. If the - server does not support it, the connection will be refused. - This is the default for SSL-enabled streams. -* If set to `enable`, the connection will use TLS if - available, falling back to an unencrypted connection if the server - does not support it. -* If set to `disable`, the connection will never use TLS. - -If you're aiming for security, then use `require` (the default). - -If you are using `enable`, you can employ -[refmem connection uses_ssl] to query whether the connection -uses SSL or not. - -This parameter is ignored for non-SSL connections. In this case, TLS will never -be used. - -See [link mysql.connparams this section] for more information on [reflink handshake_params]. - - -[endsect] \ No newline at end of file diff --git a/doc/qbk/15_error_handling.qbk b/doc/qbk/14_error_handling.qbk similarity index 100% rename from doc/qbk/15_error_handling.qbk rename to doc/qbk/14_error_handling.qbk diff --git a/doc/qbk/14_other_streams.qbk b/doc/qbk/14_other_streams.qbk deleted file mode 100644 index 0549f714d..000000000 --- a/doc/qbk/14_other_streams.qbk +++ /dev/null @@ -1,84 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:other_streams UNIX sockets and other stream types] - -The [reflink connection] class is templatized on the stream type. -Any object fulfilling the __Stream__ concept may be used -as template argument. - -[heading Convenience type aliases] - -This library provides helper type aliases for the most common cases: - -[table - [ - [Transport] - [Stream type] - [Type alias] - ] - [ - [SSL over TCP] - [`boost::asio::ssl::stream`] - [ - [reflink tcp_ssl_connection] - ] - ] - [ - [Plaintext TCP] - [`boost::asio::ip::tcp::socket`] - [ - [reflink tcp_connection] - ] - ] - [ - [UNIX sockets] - [`boost::asio::local::stream_protocol::socket`] - [ - [reflink unix_connection] - - Only available if `BOOST_ASIO_HAS_LOCAL_SOCKETS` is defined. - ] - ] -] - - -[link mysql.examples.unix_socket This example] employs a UNIX -domain socket to establish a connection to a MySQL server. - -[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]: - -* 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. - -[endsect] \ No newline at end of file diff --git a/doc/qbk/15_sql_formatting_advanced.qbk b/doc/qbk/15_sql_formatting_advanced.qbk new file mode 100644 index 000000000..d46e5a308 --- /dev/null +++ b/doc/qbk/15_sql_formatting_advanced.qbk @@ -0,0 +1,498 @@ +[/ + 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:sql_formatting_advanced Advanced client-side SQL query formatting] +[nochunk] + + +[section:expand Formatting queries without executing them] + +`with_params` is handy, but may fall short in some cases involving queries with +complex logic. For these cases, you can use [reflink format_sql] and +[reflink format_sql_to] to expand a query without executing it. +These APIs don't involve communication with the server. + +[reflink format_sql] is the simplest, and is akin to `std::format`: + +[sql_formatting_format_sql] + +`format_sql` requires a [reflink format_options] instance describing +connection configuration, like the character set currently in use. +[refmem any_connection format_opts] provides an easy way to retrieve these. +[link mysql.sql_formatting_advanced.format_options This section] contains more info about `format_opts`. + +Some use cases, usually involving conditionals, may not be +expressible in terms of a single format string. In such cases, you can +use [reflink format_context] and [reflink format_sql_to] to +build query strings incrementally: + +[sql_formatting_incremental_fn] +[sql_formatting_incremental_use] + +[reflink sequence] uses this feature to make formatting ranges easier. + +Any type that works with `with_params` also does with `format_sql` +and `format_sql_to`. These types are said to satisfy the [reflink Formattable] concept. +[link mysql.sql_formatting_advanced.reference This table] summarizes such types. + +[endsect] + + + + + + +[section:ranges Formatting ranges with sequence] + +The [reflink sequence] function can be used when the default range formatting isn't sufficient. +If the elements in your range are not formattable, you can pass a user-defined function to `sequence` +describing how to format each element: + +[sql_formatting_sequence_1] + +By default, elements are separated by commas, but this is configurable: + +[sql_formatting_sequence_2] + +You can use `sequence` and [reflink with_params] together. + +By default, `sequence` copies the range you pass as parameter, +making it safer for async code. +You can use `std::reference_wrapper` or `std::span` to avoid such copies. + +[endsect] + + + + +[section Format specifiers] + +Some types, like strings, can be formatted in multiple ways. As with +`std::format`, you can select how to format them using format specifiers. + +As we've seen, strings are formatted as single-quoted values by default. +If you use the `{:i}` specifier, you can obtain dynamic SQL identifiers, instead: + +[sql_formatting_specifiers] + +Specifiers are compatible with explicit indices and named arguments, too. +This is equivalent to the previous snippet: + +[sql_formatting_specifiers_explicit_indices] + +[endsect] + + + + +[section Extending format_sql] + +You can specialize [reflink formatter] to add formatting support to your types: + +[sql_formatting_formatter_specialization] + +The type can now be used in [reflink format_sql], [reflink format_sql_to] and [reflink with_params]: + +[sql_formatting_formatter_use] + +You can add support for format specifiers for your type by modifying +the `parse` function in `formatter`. For example, an `employee` can be formatted +differently depending on whether we're using it in an `INSERT` or an `UPDATE`: + +[sql_formatting_formatter_specialization_specifiers] + +We can now use it like this: + +[sql_formatting_formatter_use_specifiers] + +See the [reflink formatter] reference docs for more info. + +[endsect] + + + + +[heading:format_string_syntax Format string syntax] + +This section extends on the supported syntax for format strings. +The syntax is similar to the one in `fmtlib`. + +A format string is composed of regular text and replacement fields. +Regular text is output verbatim, while replacement fields are substituted +by formatted arguments. For instance, in `"SELECT {} FROM employee"`, +`"SELECT "` and `" FROM EMPLOYEE"` is regular text, and `"{}"` is a replacement field. + +A `{}` is called an [*automatic indexing] replacement field. Arguments are replaced +in the order they were provided to the format function. For instance: + +[sql_formatting_auto_indexing] + +A field index can be included within the braces. This is called [*manual indexing]. +Indices can appear in any order, and can be repeated: + +[sql_formatting_manual_indices] + +Format strings can use either manual or automatic indexing, but can't mix them: + +[sql_formatting_manual_auto_mix] + +Unreferenced format arguments are ignored. It's not an error to supply more +format arguments than required: + +[sql_formatting_unused_args] + +You can output a brace literal by doubling it: + +[sql_formatting_brace_literal] + +Format specifiers (e.g. `{:i}`) are supported for some types, +but are far less common than in fmtlib, since most types have a +single, canonical representation. + +Specifiers can appear when doing automatic indexing (e.g. `{:i}`) or +manual indexing (e.g. `{0:i}`). + +Types specializing formatters can define custom specifiers. +Only printable ASCII characters that are not `{` or `}` can be used as specifiers. + +Format strings must be encoded according to [refmem format_options charset]. +Otherwise, an error will be generated. + + + + + + +[heading:error_handling Error handling model] + +Some values can't be securely formatted. For instance, C++ +`double` can be NaN and infinity, which is not supported by MySQL. +Strings can contain byte sequences that don't represent valid characters, +which makes them impossible to escape securely. + +When using [reflink with_params] and any of these errors is encountered, +the [refmemunq any_connection execute] operation fails, as if a server error +had been encountered. This is transparent to the user, so no action is required. + +[reflink format_sql] reports these errors by throwing `boost::system::system_error` exceptions, +which contain an error code with details about what happened. For instance: + +[sql_formatting_format_double_error] + +You don't have to use exceptions, though. [reflink basic_format_context] and +[reflink format_sql_to] use [link mysql.error_handling.system_result `boost::system::result`], +instead. + +[reflink basic_format_context] contains an error code that is set when formatting +a value fails. This is called the ['error state], and can be queried using [refmem format_context_base error_state]. +When [refmem basic_format_context get] is called (after all individual values have been formatted), +the error state is checked. The `system::result` returned by `get` will contain the error +state if it was set, or the generated query if it was not: + +[sql_formatting_no_exceptions] + +Rationale: the error state mechanism makes composing formatters easier, +as the error state is checked only once. + +Errors caused by invalid format strings are also reported using this mechanism. + + + + + + +[heading:format_options Format options and character set tracking] + +MySQL has many configuration options that affect its syntax. There are two options +that formatting functions need to know in order to work: + +* Whether the backslash character represents an escape sequence or not. By default it does, + but this can be disabled dynamically by setting the + [@https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_no_backslash_escapes NO_BACKSLASH_ESCAPES] SQL mode. + This is tracked by [reflink any_connection] automatically (see [refmem any_connection backslash_escapes]). +* The connection's [*current character set]. This determines which multi-byte sequences are valid, + and is required to iterate and escape the string. The current character set is tracked + by connections as far as possible, but deficiencies in the protocol create cases where the character + set may not be known to the client. The current character set can be accessed using + [refmem any_connection current_character_set]. + +[refmem any_connection format_opts] is a convenience function that returns a +[link mysql.error_handling.system_result `boost::system::result`]`<`[reflink format_options]`>`. +If the connection could not determine the current character set, the result will contain an error. +For a reference on how character set tracking works, please read [link mysql.charsets.tracking this section]. + +[note + Prior to connection establishment, the connection's character set is always unknown. + Connect your connection before calling `format_opts`. +] + +[warning + Passing an incorrect `format_options` value to formatting functions may cause + escaping to generate incorrect values, which may generate vulnerabilities. + Stay safe and always use [refmem any_connection format_opts] instead of + hand-crafting `format_options` values. Doing this, if the character set can't be safely + determined, you will get a `client_errc::unknown_character_set` error instead of a vulnerability. +] + + + + + + +[heading Custom string types] + +[reflink format_sql_to] can be used with string types that are not `std::string`, +as long as they satisfy the [reflink OutputString] concept. This includes +strings with custom allocators (like `std::pmr::string`) and `boost::static_string`. +You need to use [reflink basic_format_context], specifying the string type: + +[sql_formatting_custom_string] + + + + + +[heading Re-using string memory] + +You can pass a string value to the context's constructor, to re-use memory: + +[sql_formatting_memory_reuse] + + + + + +[heading Raw string escaping] + +If you're building a SQL framework, or otherwise performing very low-level tasks, you may need +to just escape a string, without quoting or formatting. You can use [reflink escape_string], +which mimics [@https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-escape-string.html `mysql_real_escape_string`]. + +[note + Don't use this unless you know what you're doing. +] + + + + +[section:reference Types with built-in support for SQL formatting] + +[table + [ + [C++ type] + [Formatted as...] + [Example] + ] + [ + [`signed char`, `short`, `int`, `long`, `long long`] + [ + Integral literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_signed] + ] + ] + [ + [`unsigned char`, `unsigned short`, `unsigned int`, `unsigned long`, `unsigned long long`] + [ + Integral literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_unsigned] + ] + ] + [ + [`bool`] + [ + Integral literal `1` if `true`, `0` if `false`[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_bool] + ] + ] + [ + [ + String types (convertible to [reflink string_view]), including:[br][br] + `std::string`[br][br] + [reflink string_view][br][br] + `std::string_view`[br][br] + `const char*`[br][br] + ] + [ + Without format specifiers: single-quoted escaped string literal. Note that `LIKE` special characters (`%` and `_`) are not escaped.[br][br] + [*`i`] format specifier: backtick-quoted, escaped SQL identifier.[br][br] + [*`r`] format specifier: raw, unescaped SQL. [*Warning]: use this specifier with caution. + ] + [ + [sql_formatting_reference_string] + ] + ] + [ + [ + Blob types (convertible to `span`), including:[br][br] + [reflink blob] (`std::vector`)[br][br] + [reflink blob_view] (`span`)[br][br] + `std::array` + ] + [ + Hex string literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_blob] + ] + ] + [ + [`float`, except NaN and inf] + [ + Floating-point literal, after casting to `double.`[br] + MySQL does not support NaNs and infinities. Attempting to format + these cause a `client_errc::unformattable_value` error.[br] + No format specifiers allowed. + ] + [ + [sql_formatting_reference_float] + ] + ] + [ + [`double`, except NaN and inf] + [ + Floating-point literal.[br] + MySQL does not support NaNs and infinities. Attempting to format + these cause a `client_errc::unformattable_value` error.[br] + No format specifiers allowed. + ] + [ + [sql_formatting_reference_double] + ] + ] + [ + [[reflink date]] + [ + Single quoted, `DATE`-compatible string literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_date] + ] + ] + [ + [[reflink datetime]] + [ + Single quoted `DATETIME`-compatible string literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_datetime] + ] + ] + [ + [[reflink time] and `std::chrono::duration` types convertible to [reflink time]] + [ + Single quoted `TIME`-compatible string literal[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_time] + ] + ] + [ + [`std::nullptr_t`] + [ + `NULL`[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_nullptr] + ] + ] + [ + [ + `boost::optional` and `std::optional`, `T` being one of the fundamental types above.[br] + Not applicable to custom types or ranges.[br] + No format specifiers allowed + ] + [ + Formats the underlying value if there is any.[br] + `NULL` otherwise.[br] + ] + [ + [sql_formatting_reference_optional] + ] + ] + [ + [[reflink field] and [reflink field_view]] + [ + Formats the underlying value.[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_field] + ] + ] + [ + [ + Range of formattable elements. Informally, such ranges support + `std::begin()` and `std::end()`, and its iterator `operator*` + must yield one of the following: + + * A [reflink2 WritableFieldTuple WritableField] (i.e. one of the fundamental types above). + * A type with a custom formatter. + + Ranges of ranges are not supported. + Note that `vector` and similar types are formatted as blobs, not as sequences. + + See [reflink2 Formattable the Formattable concept reference] for a formal definition. + ] + [ + Formats each element in the range, separating elements with commas.[br] + Specifiers can be applied to individual elements by prefixing them with a colon (`:`) + ] + [ + [sql_formatting_reference_ranges] + ] + ] + [ + [ + [reflink format_sequence] (as returned by [reflink sequence]) + ] + [ + Formats each element in a range by calling a user-supplied function, + separating elements by a glue string (a comma by default).[br] + No format specifiers allowed + ] + [ + [sql_formatting_reference_sequence] + ] + ] + [ + [Custom type that specializes [reflink formatter]] + [ + Calls `formatter::parse` and `formatter::format`[br] + May accept user-defined format specifiers. + ] + [] + ] + [ + [[reflink formattable_ref]] + [ + Formats the underlying value. Can represent any of the types above.[br] + Accepts the same format specifiers as the underlying type. + ] + [ + [sql_formatting_reference_formattable_ref] + ] + ] +] + +[endsect] + + +[endsect] diff --git a/doc/qbk/16_connparams.qbk b/doc/qbk/16_connparams.qbk deleted file mode 100644 index e8c3e4797..000000000 --- a/doc/qbk/16_connparams.qbk +++ /dev/null @@ -1,104 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:connparams Handshake parameters] -[nochunk] - -This section discusses several aspects regarding the establishment -of a connection with the MySQL server, including a detailed -description of the parameters in [reflink handshake_params]. - -[heading Authentication] - -The parameters [refmem handshake_params username] and -[refmem handshake_params password] are mandatory. The -password is provided to __Self__ in plain text, -but it is not sent like that to the server (see below for more info). -If your password is empty, just provide an empty string. - -MySQL implements several methods of authentication with the server, -in what is called [mysqllink pluggable-authentication.html -pluggable authentication]. The authentication -plugin used is chosen on a per-user basis. This information -is stored in the `mysql.user` table. Additionally, -servers define a default authentication plugin -(see [mysqllink server-system-variables.html#sysvar_authentication_policy `authentication_policy`] and -[mysqllink server-system-variables.html#sysvar_default_authentication_plugin -`default_authentication_plugin`]). The default plugin will -be used for newly created users, and may affect how the handshake works. - -__Self__ implements the two most common authentication plugins: - -* [mysqllink native-pluggable-authentication.html `mysql_native_password`]. - Unless otherwise configured, this is the default plugin for - MySQL 5.7 and MariaDB. It can be used over both TLS and non-TLS - connections. It sends the password hashed, salted by a nonce. -* [mysqllink caching-sha2-pluggable-authentication.html - `caching_sha2_password`]. Unless otherwise configured, this is the default plugin for - MySQL 8.0. It can only be used over TLS, which makes it less vulnerable. - This is also the reason why all examples use TLS. - -Other authentication plugins are not supported. Multi-factor authentication -is not yet supported, either. If you require any other plugin, please file a feature request -against the GitHub repository. - -If you try to establish a connection (using [refmem connection handshake] or -[refmem connection connect]) and you specify a user with -an unsupported authentication plugin, the operation will fail. - -[note - Servers configured with a default authentication plugin - not implemented in __Self__ are not supported, regardless - of the actual plugin the concrete user employs. This limitation - may be lifted in the future. -] - -[heading Connect with database] - -The parameter [refmem handshake_params database] is a string -with the database name to connect to. If you specify it, -your connection will default to use that database, as if -you had issued a __USE__ statement. You can leave it blank -to select no database. You can always employ a __USE__ -statement to select a different database after establishing -the connection. - -[heading Connection encoding and collation] - -When establishing a connection, you specify a numeric collation ID -parameter ([refmem handshake_params connection_collation]), which will -determine the connection's character set and collation. This determines -the encoding of the strings sent to and received from the server. -If left unspecified, `utf8mb4_general_ci` will be used, which is portable -accross MySQL 5.x, MySQL 8.x and MariaDB. - -Collation IDs are defined in [include_file boost/mysql/mysql_collations.hpp] and -[include_file boost/mysql/mariadb_collations.hpp]. Some collations are portable -between servers, while others are MySQL or MariaDB-specific, and some IDs overlap. -You may also define your own collations server-side. This is why collations -are specified as an integer, rather than an enumeration. - -Please refer to [link mysql.charsets this section] for more info -about character sets. - -[warning - If you specify a collation ID that is unknown to the server (an old server - that doesn't recognize the newest collations), the handshake operation - will succeed but the connection will sillently fall back to the server's default character set - and collation. If you want to be sure, use a `"SET NAMES"` statement. -] - -[heading SSL/TLS] - -When establising a connection, you can specify a [reflink ssl_mode] -value to configure whether to use SSL/TLS or not. As explained in -[link mysql.ssl.negotiation this section], this parameters can be -employed to configure SSL negotiation. This value is ignored if the -underlying stream does not support SSL. - - -[endsect] [/ connparams] diff --git a/doc/qbk/11_metadata.qbk b/doc/qbk/16_metadata.qbk similarity index 100% rename from doc/qbk/11_metadata.qbk rename to doc/qbk/16_metadata.qbk diff --git a/doc/qbk/18_charsets.qbk b/doc/qbk/17_charsets.qbk similarity index 92% rename from doc/qbk/18_charsets.qbk rename to doc/qbk/17_charsets.qbk index 7bf383646..eca8ba49d 100644 --- a/doc/qbk/18_charsets.qbk +++ b/doc/qbk/17_charsets.qbk @@ -31,8 +31,8 @@ string fields and column names in metadata. The connection's collation is used f Every session you establish can have its own different character set and collation. You can specify this in two ways: -* When calling [refmem connection connect] or [refmem connection handshake], using - [refmem handshake_params connection_collation]. You specify a numeric ID that identifies +* 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. @@ -41,12 +41,13 @@ character set and collation. You can specify this in two ways: (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, issuing a __SET_NAMES__ SQL statement. For example, `"SET NAMES utf8mb4"` will set the current - connection's character set to `utf8mb4` and the connection's collation to utf8mb4's default collation. - If the character set is unknown, the `SET NAMES` statement will fail. - You can use [refmem connection execute] to issue the statement: +* At any time, using [refmem any_connection async_set_character_set]. -[charsets_set_names] +[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. +] [heading character_set_results and character_set_client] @@ -125,7 +126,7 @@ The table below summarizes the encoding used by each piece of functionality in t ] ] -[heading:tracking (Experimental) Character set tracking] +[heading:tracking Character set tracking] [reflink any_connection] attempts to track the connection's current character set. You can access this information using @@ -166,7 +167,7 @@ This is how tracking works: ] -[heading:custom (Experimental) Adding support for a character set] +[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`. diff --git a/doc/qbk/17_reconnecting.qbk b/doc/qbk/17_reconnecting.qbk deleted file mode 100644 index 6da58a130..000000000 --- a/doc/qbk/17_reconnecting.qbk +++ /dev/null @@ -1,48 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:reconnecting Reconnecting a MySQL connection] - -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]. - -[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: - -* After calling [refmem connection close], you can re-open the connection later by calling - [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. - -[endsect] \ No newline at end of file diff --git a/doc/qbk/19_time_types.qbk b/doc/qbk/18_time_types.qbk similarity index 100% rename from doc/qbk/19_time_types.qbk rename to doc/qbk/18_time_types.qbk diff --git a/doc/qbk/19_templated_connection.qbk b/doc/qbk/19_templated_connection.qbk new file mode 100644 index 000000000..39d470e8a --- /dev/null +++ b/doc/qbk/19_templated_connection.qbk @@ -0,0 +1,229 @@ +[/ + 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:templated_connection The legacy connection class] +[nochunk] + +You may encounter code using [reflink connection] or its aliases, +[reflink tcp_connection], [reflink tcp_ssl_connection], +[reflink unix_connection]. This was the +main way to create client connections until Boost 1.87, when +[reflink any_connection] became stable. + +`connection` is not deprecated, but we don't recommend using it in new code. +[reflink any_connection] is simpler to use and provides the same +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. + +The library provides helper type aliases for the most common cases: + +[table + [ + [Transport] + [Stream type] + [Type alias] + ] + [ + [SSL over TCP] + [`boost::asio::ssl::stream`] + [ + [reflink tcp_ssl_connection] + ] + ] + [ + [Plaintext TCP] + [`boost::asio::ip::tcp::socket`] + [ + [reflink tcp_connection] + ] + ] + [ + [UNIX sockets] + [`boost::asio::local::stream_protocol::socket`] + [ + [reflink unix_connection] + + Only available if `BOOST_ASIO_HAS_LOCAL_SOCKETS` is defined. + ] + ] +] + +In contrast, [reflink any_connection] is not templated. +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. +For a [reflink tcp_ssl_connection], we need to pass an execution context and +a __ssl_context__: + +[templated_connection_creation] + + + + + +[heading Connection establishment and termination] + +When using TCP, + +* 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 + +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`. + +When compared to [reflink connection], `any_connection`: + +* 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. + +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. + + + +[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]. + +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. + + +[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]: + +* 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. + + +[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]. + +[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: + +* After calling [refmem connection close], you can re-open the connection later by calling + [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. + + +[endsect] diff --git a/doc/qbk/20_any_connection.qbk b/doc/qbk/20_any_connection.qbk deleted file mode 100644 index e8bb7d993..000000000 --- a/doc/qbk/20_any_connection.qbk +++ /dev/null @@ -1,83 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:any_connection Type-erased connections with any_connection] -[nochunk] - -[reflink any_connection] is a type-erased alternative to [reflink connection]. -It's easier to use and features more functionality than plain `connection`. - -When compared to [reflink connection], `any_connection`: - -* 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 Example] - -To connect to a server using TCP, use the following: - -[any_connection_tcp] - -[refmem connect_params server_address] is an [reflink any_address], which is -a variant-like type that can hold a (hostname, port) pair or a UNIX socket path. -For example, to connect using a UNIX socket: - -[any_connection_unix] - - - -[heading Reconnection] - -[reflink any_connection] can always be reconnected. [refmem any_connection connect] -and [refmem any_connection async_connect] will wipe any previous connection state. -This works even if an error or a timeout occurred, as opposed to `connection`. - -For instance, the following can be used to implement retries for connection establishment: - -[any_connection_reconnect] - -Likewise, if you encounter a fatal error (like a network error), just call `connect` or `async_connect`. - -If you need reliable, long-lived connections, consider [link mysql.connection_pool using a connection pool] -instead of rolling out your own strategy. - - - - - -[heading TLS] - -By default, [reflink any_connection] uses TLS when using TCP connections and the -server supports it. You can change this setting using [refmem connect_params ssl]: - -[any_connection_ssl_mode] - -See [link mysql.ssl.negotiation this section] for more info on MySQL TLS negotiation. - -By default, `any_connection` will create an [asioreflink ssl__context ssl::context] object with suitable -default options for you, if required. If you want to configure TLS options beyond defaults, -you can pass your own context to `any_connection`'s constructor, and -it will be used: - -[any_connection_ssl_ctx] - - -[endsect] \ No newline at end of file diff --git a/doc/qbk/24_pipeline.qbk b/doc/qbk/20_pipeline.qbk similarity index 100% rename from doc/qbk/24_pipeline.qbk rename to doc/qbk/20_pipeline.qbk diff --git a/doc/qbk/21_examples.qbk b/doc/qbk/21_examples.qbk new file mode 100644 index 000000000..de03d4bc0 --- /dev/null +++ b/doc/qbk/21_examples.qbk @@ -0,0 +1,324 @@ +[/ + 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) +] + +[/ This file was auto-generated by examples_qbk.py. Do not edit directly ] + +[section:examples Examples] + +To run the examples, please go through the [link mysql.examples.setup setup] first. + +Here is a list of available examples: + +[heading Tutorials] + +Self-contained programs demonstrating the basic concepts. + +* [link mysql.examples.tutorial_sync Tutorial 1 listing: hello world!] +* [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] + +[heading Simple programs] + +Self-contained programs demonstrating more advanced concepts and techniques. + +* [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] +* [link mysql.examples.multi_function Reading rows in batches with multi-function operations] +* [link mysql.examples.callbacks Callbacks (async functions in C++11)] +* [link mysql.examples.coroutines_cpp11 Stackful coroutines (async functions in C++11)] +* [link mysql.examples.unix_socket UNIX sockets] +* [link mysql.examples.batch_inserts Batch inserts using client-side query formatting] +* [link mysql.examples.batch_inserts_generic Generic batch inserts with Boost.Describe] +* [link mysql.examples.dynamic_filters Queries with dynamic filters] +* [link mysql.examples.patch_updates Dynamic UPDATE queries with PATCH-like semantics] +* [link mysql.examples.source_script Sourcing a .sql file using multi-queries] +* [link mysql.examples.pipeline (Experimental) Pipelines] + +[heading Advanced examples] + +Programs implementing real-world functionality. + + +# [@https://github.com/anarthal/servertech-chat The BoostServerTech chat project uses Boost.MySQL and Boost.Redis to implement a chat server] + +[heading Setup] + +To run the examples, you need a MySQL server you can connect to. +Examples make use of a database named `boost_mysql_examples`. +The server hostname and credentials (username and password) are passed +to the examples via the command line. + +You can spin up a server quickly by using Docker: + +[!teletype] +``` + # Remove the "-v /var/run/mysqld:/var/run/mysqld" part if you don't need UNIX sockets + > docker run --name some-mysql -p 3306:3306 -v /var/run/mysqld:/var/run/mysqld -d -e MYSQL_ROOT_PASSWORD= -e MYSQL_ALLOW_EMPTY_PASSWORD=1 -d mysql + + # All the required data can be loaded by running example/db_setup.sql. + # If you're using the above container, the root user has a blank password + > mysql -u root < example/db_setup.sql +``` + +Please note that this container is just for demonstrative purposes, +and is not suitable for production. + +The root MySQL user for these containers is `root` and has an empty password. + + +[section:tutorial_sync Tutorial 1 listing: hello world!] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_tutorial_sync] + +[endsect] + + + + +[section:tutorial_async Tutorial 2 listing: going async with C++20 coroutines] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_tutorial_async] + +[endsect] + + + + +[section:tutorial_with_params Tutorial 3 listing: queries with parameters] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_tutorial_with_params] + +[endsect] + + + + +[section:tutorial_static_interface Tutorial 4 listing: the static interface] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_tutorial_static_interface] + +[endsect] + + + + +[section:prepared_statements Prepared statements] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_prepared_statements] + +[endsect] + + + + +[section:timeouts Setting timeouts to operations] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_timeouts] + +[endsect] + + + + +[section:multi_queries_transactions Using multi-queries and transactions] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_multi_queries_transactions] + +[endsect] + + + + +[section:disable_tls Disabling TLS for a connection] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_disable_tls] + +[endsect] + + + + +[section:tls_certificate_verification Setting TLS options: enabling TLS certificate verification] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_tls_certificate_verification] + +[endsect] + + + + +[section:metadata Metadata] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_metadata] + +[endsect] + + + + +[section:multi_function Reading rows in batches with multi-function operations] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_multi_function] + +[endsect] + + + + +[section:callbacks Callbacks (async functions in C++11)] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_callbacks] + +[endsect] + + + + +[section:coroutines_cpp11 Stackful coroutines (async functions in C++11)] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_coroutines_cpp11] + +[endsect] + + + + +[section:unix_socket UNIX sockets] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_unix_socket] + +[endsect] + + + + +[section:batch_inserts Batch inserts using client-side query formatting] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_batch_inserts] + +[endsect] + + + + +[section:batch_inserts_generic Generic batch inserts with Boost.Describe] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_batch_inserts_generic] + +[endsect] + + + + +[section:dynamic_filters Queries with dynamic filters] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_dynamic_filters] + +[endsect] + + + + +[section:patch_updates Dynamic UPDATE queries with PATCH-like semantics] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_patch_updates] + +[endsect] + + + + +[section:source_script Sourcing a .sql file using multi-queries] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_source_script] + +[endsect] + + + + +[section:pipeline (Experimental) Pipelines] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_pipeline] + +[endsect] + + + + +[section:connection_pool A REST API server that uses connection pooling] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[example_connection_pool_main_cpp] + +[example_connection_pool_types_hpp] + +[example_connection_pool_repository_hpp] + +[example_connection_pool_repository_cpp] + +[example_connection_pool_handle_request_hpp] + +[example_connection_pool_handle_request_cpp] + +[example_connection_pool_server_hpp] + +[example_connection_pool_server_cpp] + +[example_connection_pool_log_error_hpp] + +[endsect] + + +[endsect] + diff --git a/doc/qbk/22_sql_formatting.qbk b/doc/qbk/22_sql_formatting.qbk deleted file mode 100644 index 5111236cc..000000000 --- a/doc/qbk/22_sql_formatting.qbk +++ /dev/null @@ -1,513 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:sql_formatting Client-side SQL query formatting] -[nochunk] - -Client-side SQL formatting allows running SQL queries with user-supplied parameters securely. -It can be used as a simpler and more flexible alternative to prepared statements. -While prepared statements expand queries server-side, SQL formatting does it client-side. -Please read the [link mysql.sql_formatting.comparison comparison with prepared statements] -and the [link mysql.sql_formatting.security security considerations] sections for more info. - - - - - - -[heading Simple queries] - -[reflink with_params] takes a SQL query -string with placeholders and a set of parameters. When passed to -[refmemunq any_connection execute] or [refmemunq any_connection async_execute], -the query is expanded in the client with the supplied parameters and -sent to the server for execution: - -[sql_formatting_simple] - -Curly braces (`{}`) represent placeholders (technically called ['replacement fields]). -The notation and semantics are similar to [@https://en.cppreference.com/w/cpp/utility/format/format `std::format`]. - -All fundamental types can be used as query parameters. This includes integers, floating point types, -strings, blobs, dates and times: - -[sql_formatting_other_scalars] - -`std::optional` and `boost::optional` can also be used: - -[sql_formatting_optionals] - -Collections and ranges are supported, as long as its elements can be formatted: - -[sql_formatting_ranges] - -See [link mysql.sql_formatting.ranges this section] for more on formatting ranges, -and [link mysql.sql_formatting.reference this table] for a reference of types -that have built-in support for SQL formatting. - -[note - Like with `std::format`, the query string passed to `with_params` must be known at - compile-time. You can skip this check using the [reflink runtime] function. -] - -Like `std::format`, you can use arguments with explicit indices: - -[sql_formatting_manual_indices] - -See [link mysql.sql_formatting_advanced.format_string_syntax this section] -for a reference on the format string syntax. - - - - - - -[heading:errors Common errors and how to fix them] - -Not all values can be formatted. If the library finds that formatting a certain -value can cause an ambiguity that could lead to a security problem, an error -will be issued and the query won't be sent to the server. Here are the most common errors: - -* `client_errc::invalid_encoding` - * Cause: one of your string parameters contains invalid code points. - With the default character set, this means that it contains [*invalid UTF-8]. - * Solution: all string values must be encoded according to the connection's character - set (usually UTF-8). Sanitize or reject such values. Use the [reflink blob] and [reflink blob_view] - types for values that don't represent character strings, but arbitrary binary values. -* `client_errc::unformattable_value` - * Cause: one of your parameters contains an invalid value. For instance, a `double` - contains a `NaN` or an `Inf`, unsupported by MySQL. - * Solution: reject such values, or replace them by `NULL` before passing them to client-side SQL formatting. -* `client_errc::unknown_character_set` - * Cause: your connection doesn't know the character set you're using. Knowing the character set in use - is required to generate queries securely. This situation can happen after calling [refmemunq any_connection reset_connection] - or if you used a custom [refmem connect_params connection_collation] when connecting. - * Solution: use a [reflink connection_pool] instead of manually resetting connections. If you can't, - use the default [refmemunq connect_params connection_collation] when connecting, and use - [refmemunq any_connection set_character_set] or [refmemunq any_connection async_set_character_set] - after resetting connections. - * [link mysql.charsets.tracking Learn more] about how character set tracking works. - -For example: - -[sql_formatting_invalid_encoding] - - - - -[heading Formatting queries without executing them] - -`with_params` is handy, but may fall short in some cases involving queries with -complex logic. For these cases, you can use [reflink format_sql] and -[reflink format_sql_to] to expand a query without executing it. -These APIs don't involve communication with the server. - -[reflink format_sql] is the simplest, and is akin to `std::format`: - -[sql_formatting_format_sql] - -`format_sql` requires a [reflink format_options] instance describing -connection configuration, like the character set currently in use. -[refmem any_connection format_opts] provides an easy way to retrieve these. -[link mysql.sql_formatting_advanced.format_options This section] contains more info about `format_opts`. - -Some use cases, usually involving conditionals, may not be -expressible in terms of a single format string. In such cases, you can -use [reflink format_context] and [reflink format_sql_to] to -build query strings incrementally: - -[sql_formatting_incremental_fn] -[sql_formatting_incremental_use] - -[reflink sequence] uses this feature to make formatting ranges easier. - -Any type that works with `with_params` also does with `format_sql` -and `format_sql_to`. These types are said to satisfy the [reflink Formattable] concept. -[link mysql.sql_formatting.reference This table] summarizes such types. - - - - - - - - - - - - - -[heading:ranges Formatting ranges with sequence] - -The [reflink sequence] function can be used when the default range formatting isn't sufficient. -If the elements in your range are not formattable, you can pass a user-defined function to `sequence` -describing how to format each element: - -[sql_formatting_sequence_1] - -By default, elements are separated by commas, but this is configurable: - -[sql_formatting_sequence_2] - -You can use `sequence` and [reflink with_params] together. - -By default, `sequence` copies the range you pass as parameter, -making it safer for async code. -You can use `std::reference_wrapper` or `std::span` to avoid such copies. - - - - - - - -[heading Format specifiers] - -Some types, like strings, can be formatted in multiple ways. As with -`std::format`, you can select how to format them using format specifiers. - -As we've seen, strings are formatted as single-quoted values by default. -If you use the `{:i}` specifier, you can obtain dynamic SQL identifiers, instead: - -[sql_formatting_specifiers] - -Specifiers are compatible with explicit indices and named arguments, too. -This is equivalent to the previous snippet: - -[sql_formatting_specifiers_explicit_indices] - - - - - - -[heading:comparison Prepared statements vs. client-side SQL formatting] - -Although both serve a similar purpose, they are fundamentally different. Prepared statements -are parsed and expanded by the server. Client-side SQL expands the query in the client -and sends it to the server as a string. - -This means that [*client-side SQL does not understand your queries]. It just knows about how -to format MySQL types into a string without creating vulnerabilities, but otherwise treats -your queries as opaque strings. Client-side SQL yields [*greater flexibility] (you can dynamically -compose any query), while statements have more limitations. This also means that -[*you need to pay more attention to compose valid queries], specially when dealing with complex conditionals. -Logic errors may lead to exploits. Please read the [link mysql.sql_formatting.security security considerations section] -for more info. - -Client-side SQL entails [*less round-trips to the server] than statements, and is usually more efficient -for lightweight queries. However, it uses the less compact text protocol, which may be slower for -queries retrieving a lot of data. See the [link mysql.sql_formatting.efficiency efficiency considerations section] for more info. - -In general, [*use client-side SQL] formatting for the following cases: - -* Simple queries that don't retrieve a lot of data. Default to `with_params` and - only switch to statements if your performance measurements says so. -* Queries involving dynamic SQL that can't be achieved by statements. Typical cases include: - * Dynamic filters ([link mysql.examples.dynamic_filters example]). - * Batch inserts. Inserting rows one by one can lead to poor efficiency. - You can use client-side SQL formatting to compose a single `INSERT` that - inserts several rows at once (see [link mysql.examples.batch_inserts example 1] - and [link mysql.examples.batch_inserts_generic example 2]). - * PATCH-like updates, where the field list in an `UPDATE` must be dynamic - ([link mysql.examples.patch_updates example]). - * Queries involving dynamic identifiers, like table and field names. - * Conditional sorting. - * Pipelines consisting of several semicolon-separated queries with dynamic fields. - -On the other hand, [*prefer prepared statements] if: - -* You are executing the same query over and over. You can prepare the statement - once and execute it several times. -* Your query is retrieving a lot of data, and you have performed the relevant performance measurements. - - - -[heading:efficiency Efficiency considerations] - -Both client-side SQL formatting and prepared statements have pros and cons efficiency-wise: - -* Client-formatted SQL entails [*less round-trips to the server]. For prepared statements, you usually need - a call to prepare the statement, another one to execute it, and possibly a final one to close it. - Client-formatted SQL only requires the execution round-trip. This performance gain increases with network - latency and if you are using TLS. -* Prepared statements always entail a [*mutation of session state], while client-formatted SQL may not. - If you're using a [reflink connection_pool] with prepared statements, you can't use - [refmem pooled_connection return_without_reset], as this will leak the statement. - With client-formatted queries, reset may not be required if your SQL doesn't mutate session state. -* Client-formatted SQL queries use a usually [*less efficient text-based protocol], while prepared statements - use a more compact binary protocol. This is relevant if you're retrieving lots of data that is - slow to convert to and from text (like doubles). -* [*Prepared statements can be re-used]. If you need to execute a query several times, - prepared statements will only be parsed once. -* Client-formatted SQL allows [*more efficient patterns] than prepared statements, - like batch inserts and semicolon-separated queries. - - - - - - -[heading:security Security considerations] - -Both client-side SQL formatting and prepared statements [*protect against SQL injection]. -Statements do so by parsing the query with placeholders server-side, before performing parameter -substitution. Client-side SQL quotes and escapes your values to avoid injection, but -[*does not understand your queries]. - -This means that you need to [*ensure that your queries always expand to valid SQL]. -This is trivial for simple queries, but may be an issue with more complex ones, -involving ranges or dynamic identifiers. For instance, the following query may -expand to invalid SQL if the provided range is empty: - -[sql_formatting_empty_ranges] - -The risk is higher if you're building your query by pieces using [reflink format_sql_to]. - -To sum up: - -* Client-side SQL protects against SQL injection. -* Client-side SQL does not protect against logic errors. The risk is only present in complex - queries. We suggest the following advice: - * Avoid complex query generation logic as much as possible. - Use a single format string instead of `format_sql_to`, unless you have no other option. - * When using ranges, consider if the empty range would lead to valid SQL or not. - * Thoroughly test complex query generation logic. -* Client-side SQL requires knowing the connection's current character set. This usually happens - out of the box, and will lead to a [link mysql.sql_formatting.errors controlled error] - otherwise. Some recommendations: - * If in doubt, always use the default character set (`utf8mb4`). - * Never issue `SET NAMES` or `SET CHARACTER SET` statements directly - - use [refmem any_connection set_character_set] or [refmemunq any_connection async_set_character_set], instead. - * If you're using [reflink format_sql] or [reflink format_sql_to], never craft [reflink format_options] values manually. - Use [refmem any_connection format_opts], instead. - - - - - - - - - - - - -[heading:reference Types with built-in support for SQL formatting] - -[table - [ - [C++ type] - [Formatted as...] - [Example] - ] - [ - [`signed char`, `short`, `int`, `long`, `long long`] - [ - Integral literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_signed] - ] - ] - [ - [`unsigned char`, `unsigned short`, `unsigned int`, `unsigned long`, `unsigned long long`] - [ - Integral literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_unsigned] - ] - ] - [ - [`bool`] - [ - Integral literal `1` if `true`, `0` if `false`[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_bool] - ] - ] - [ - [ - String types (convertible to [reflink string_view]), including:[br][br] - `std::string`[br][br] - [reflink string_view][br][br] - `std::string_view`[br][br] - `const char*`[br][br] - ] - [ - Without format specifiers: single-quoted escaped string literal. Note that `LIKE` special characters (`%` and `_`) are not escaped.[br][br] - [*`i`] format specifier: backtick-quoted, escaped SQL identifier.[br][br] - [*`r`] format specifier: raw, unescaped SQL. [*Warning]: use this specifier with caution. - ] - [ - [sql_formatting_reference_string] - ] - ] - [ - [ - Blob types (convertible to `span`), including:[br][br] - [reflink blob] (`std::vector`)[br][br] - [reflink blob_view] (`span`)[br][br] - `std::array` - ] - [ - Hex string literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_blob] - ] - ] - [ - [`float`, except NaN and inf] - [ - Floating-point literal, after casting to `double.`[br] - MySQL does not support NaNs and infinities. Attempting to format - these cause a `client_errc::unformattable_value` error.[br] - No format specifiers allowed. - ] - [ - [sql_formatting_reference_float] - ] - ] - [ - [`double`, except NaN and inf] - [ - Floating-point literal.[br] - MySQL does not support NaNs and infinities. Attempting to format - these cause a `client_errc::unformattable_value` error.[br] - No format specifiers allowed. - ] - [ - [sql_formatting_reference_double] - ] - ] - [ - [[reflink date]] - [ - Single quoted, `DATE`-compatible string literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_date] - ] - ] - [ - [[reflink datetime]] - [ - Single quoted `DATETIME`-compatible string literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_datetime] - ] - ] - [ - [[reflink time] and `std::chrono::duration` types convertible to [reflink time]] - [ - Single quoted `TIME`-compatible string literal[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_time] - ] - ] - [ - [`std::nullptr_t`] - [ - `NULL`[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_nullptr] - ] - ] - [ - [ - `boost::optional` and `std::optional`, `T` being one of the fundamental types above.[br] - Not applicable to custom types or ranges.[br] - No format specifiers allowed - ] - [ - Formats the underlying value if there is any.[br] - `NULL` otherwise.[br] - ] - [ - [sql_formatting_reference_optional] - ] - ] - [ - [[reflink field] and [reflink field_view]] - [ - Formats the underlying value.[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_field] - ] - ] - [ - [ - Range of formattable elements. Informally, such ranges support - `std::begin()` and `std::end()`, and its iterator `operator*` - must yield one of the following: - - * A [reflink2 WritableFieldTuple WritableField] (i.e. one of the fundamental types above). - * A type with a custom formatter. - - Ranges of ranges are not supported. - Note that `vector` and similar types are formatted as blobs, not as sequences. - - See [reflink2 Formattable the Formattable concept reference] for a formal definition. - ] - [ - Formats each element in the range, separating elements with commas.[br] - Specifiers can be applied to individual elements by prefixing them with a colon (`:`) - ] - [ - [sql_formatting_reference_ranges] - ] - ] - [ - [ - [reflink format_sequence] (as returned by [reflink sequence]) - ] - [ - Formats each element in a range by calling a user-supplied function, - separating elements by a glue string (a comma by default).[br] - No format specifiers allowed - ] - [ - [sql_formatting_reference_sequence] - ] - ] - [ - [Custom type that specializes [reflink formatter]] - [ - Calls `formatter::parse` and `formatter::format`[br] - May accept user-defined format specifiers. - ] - [] - ] - [ - [[reflink formattable_ref]] - [ - Formats the underlying value. Can represent any of the types above.[br] - Accepts the same format specifiers as the underlying type. - ] - [ - [sql_formatting_reference_formattable_ref] - ] - ] -] - -[endsect] \ No newline at end of file diff --git a/doc/qbk/23_sql_formatting_advanced.qbk b/doc/qbk/23_sql_formatting_advanced.qbk deleted file mode 100644 index 1b1501174..000000000 --- a/doc/qbk/23_sql_formatting_advanced.qbk +++ /dev/null @@ -1,200 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:sql_formatting_advanced Advanced client-side SQL query formatting] -[nochunk] - - -[heading Extending format_sql] - -You can specialize [reflink formatter] to add formatting support to your types: - -[sql_formatting_formatter_specialization] - -The type can now be used in [reflink format_sql], [reflink format_sql_to] and [reflink with_params]: - -[sql_formatting_formatter_use] - -You can add support for format specifiers for your type by modifying -the `parse` function in `formatter`. For example, an `employee` can be formatted -differently depending on whether we're using it in an `INSERT` or an `UPDATE`: - -[sql_formatting_formatter_specialization_specifiers] - -We can now use it like this: - -[sql_formatting_formatter_use_specifiers] - -See the [reflink formatter] reference docs for more info. - - - - - - -[heading:format_string_syntax Format string syntax] - -This section extends on the supported syntax for format strings. -The syntax is similar to the one in `fmtlib`. - -A format string is composed of regular text and replacement fields. -Regular text is output verbatim, while replacement fields are substituted -by formatted arguments. For instance, in `"SELECT {} FROM employee"`, -`"SELECT "` and `" FROM EMPLOYEE"` is regular text, and `"{}"` is a replacement field. - -A `{}` is called an [*automatic indexing] replacement field. Arguments are replaced -in the order they were provided to the format function. For instance: - -[sql_formatting_auto_indexing] - -A field index can be included within the braces. This is called [*manual indexing]. -Indices can appear in any order, and can be repeated: - -[sql_formatting_manual_indices] - -Format strings can use either manual or automatic indexing, but can't mix them: - -[sql_formatting_manual_auto_mix] - -Unreferenced format arguments are ignored. It's not an error to supply more -format arguments than required: - -[sql_formatting_unused_args] - -You can output a brace literal by doubling it: - -[sql_formatting_brace_literal] - -Format specifiers (e.g. `{:i}`) are supported for some types, -but are far less common than in fmtlib, since most types have a -single, canonical representation. - -Specifiers can appear when doing automatic indexing (e.g. `{:i}`) or -manual indexing (e.g. `{0:i}`). - -Types specializing formatters can define custom specifiers. -Only printable ASCII characters that are not `{` or `}` can be used as specifiers. - -Format strings must be encoded according to [refmem format_options charset]. -Otherwise, an error will be generated. - - - - - -[heading:error_handling Error handling model] - -Some values can't be securely formatted. For instance, C++ -`double` can be NaN and infinity, which is not supported by MySQL. -Strings can contain byte sequences that don't represent valid characters, -which makes them impossible to escape securely. - -When using [reflink with_params] and any of these errors is encountered, -the [refmemunq any_connection execute] operation fails, as if a server error -had been encountered. This is transparent to the user, so no action is required. - -[reflink format_sql] reports these errors by throwing `boost::system::system_error` exceptions, -which contain an error code with details about what happened. For instance: - -[sql_formatting_format_double_error] - -You don't have to use exceptions, though. [reflink basic_format_context] and -[reflink format_sql_to] use [link mysql.error_handling.system_result `boost::system::result`], -instead. - -[reflink basic_format_context] contains an error code that is set when formatting -a value fails. This is called the ['error state], and can be queried using [refmem format_context_base error_state]. -When [refmem basic_format_context get] is called (after all individual values have been formatted), -the error state is checked. The `system::result` returned by `get` will contain the error -state if it was set, or the generated query if it was not: - -[sql_formatting_no_exceptions] - -Rationale: the error state mechanism makes composing formatters easier, -as the error state is checked only once. - -Errors caused by invalid format strings are also reported using this mechanism. - - - - - -[heading:format_options Format options and character set tracking] - -MySQL has many configuration options that affect its syntax. There are two options -that formatting functions need to know in order to work: - -* Whether the backslash character represents an escape sequence or not. By default it does, - but this can be disabled dynamically by setting the - [@https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_no_backslash_escapes NO_BACKSLASH_ESCAPES] SQL mode. - This is tracked by [reflink any_connection] automatically (see [refmem any_connection backslash_escapes]). -* The connection's [*current character set]. This determines which multi-byte sequences are valid, - and is required to iterate and escape the string. The current character set is tracked - by connections as far as possible, but deficiencies in the protocol create cases where the character - set may not be known to the client. The current character set can be accessed using - [refmem any_connection current_character_set]. - -[refmem any_connection format_opts] is a convenience function that returns a -[link mysql.error_handling.system_result `boost::system::result`]`<`[reflink format_options]`>`. -If the connection could not determine the current character set, the result will contain an error. -For a reference on how character set tracking works, please read [link mysql.charsets.tracking this section]. - -[note - Prior to connection establishment, the connection's character set is always unknown. - Connect your connection before calling `format_opts`. -] - -[warning - Passing an incorrect `format_options` value to formatting functions may cause - escaping to generate incorrect values, which may generate vulnerabilities. - Stay safe and always use [refmem any_connection format_opts] instead of - hand-crafting `format_options` values. Doing this, if the character set can't be safely - determined, you will get a `client_errc::unknown_character_set` error instead of a vulnerability. -] - - - - - -[heading Custom string types] - -[reflink format_sql_to] can be used with string types that are not `std::string`, -as long as they satisfy the [reflink OutputString] concept. This includes -strings with custom allocators (like `std::pmr::string`) and `boost::static_string`. -You need to use [reflink basic_format_context], specifying the string type: - -[sql_formatting_custom_string] - - - - - -[heading Re-using string memory] - -You can pass a string value to the context's constructor, to re-use memory: - -[sql_formatting_memory_reuse] - - - - - -[heading Raw string escaping] - -If you're building a SQL framework, or otherwise performing very low-level tasks, you may need -to just escape a string, without quoting or formatting. You can use [reflink escape_string], -which mimics [@https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-escape-string.html `mysql_real_escape_string`]. - -[note - Don't use this unless you know what you're doing. -] - - - - - -[endsect] diff --git a/doc/qbk/24_examples.qbk b/doc/qbk/24_examples.qbk deleted file mode 100644 index 06db92061..000000000 --- a/doc/qbk/24_examples.qbk +++ /dev/null @@ -1,470 +0,0 @@ -[/ - Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - - Distributed under the Boost Software License, Version 1.0. (See accompanying - file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -] - -[section:examples Examples] - -Welcome to __Self__ examples. If you are intending to -run the examples, please go through the [link mysql.examples.setup -setup] first. - -Here is a list of available examples: - -# [link mysql.examples.async_callbacks Async functions using callbacks] -# [link mysql.examples.async_futures Async functions using futures] -# [link mysql.examples.async_coroutines Async functions using stackful coroutines] -# [link mysql.examples.async_coroutinescpp20 Async functions using C++20 coroutines] -# [link mysql.examples.timeouts Setting timeouts] -# [link mysql.examples.prepared_statements_cpp11 Using prepared statements with the dynamic interface (C++11)] -# [link mysql.examples.prepared_statements_cpp14 Using prepared statements with the static interface (C++14)] -# [link mysql.examples.stored_procedures_cpp11 Using stored procedures with the dynamic interface (C++11)] -# [link mysql.examples.stored_procedures_cpp14 Using stored procedures with the static interface (C++14)] -# [link mysql.examples.metadata Metadata] -# [link mysql.examples.unix_socket UNIX sockets] -# [link mysql.examples.ssl Setting SSL options] -# [link mysql.examples.source_script Using multi-queries to source a .sql file] -# [link mysql.examples.any_connection Using type-erased connections] -# [link mysql.examples.batch_inserts Batch inserts using client-side query formatting] -# [link mysql.examples.batch_inserts_generic Generic batch inserts with Boost.Describe: extending format_sql] -# [link mysql.examples.dynamic_filters Implements a query with several dynamic filters using client-side query formatting] -# [link mysql.examples.patch_updates Implements a dynamic UPDATE query for PATCH-like update semantics using client-side query formatting] -# [link mysql.examples.pipeline Prepares, executes and closes statements in batch using the pipeline API] -# [link mysql.examples.connection_pool A REST API server that uses connection pooling] -# [@https://github.com/anarthal/servertech-chat The BoostServerTech chat project uses Boost.MySQL and Boost.Redis to implement a chat server] - -[heading Setup] - -To run the examples, you need a MySQL server you can connect to. -Examples make use of a database named `boost_mysql_examples`. -The server hostname and credentials (username and password) are passed -to the examples via the command line. - -You can spin up a server quickly by using Docker: - -[!teletype] -``` - # Remove the "-v /var/run/mysqld:/var/run/mysqld" part if you don't need UNIX sockets - > docker run --name some-mysql -p 3306:3306 -v /var/run/mysqld:/var/run/mysqld -d -e MYSQL_ROOT_PASSWORD= -e MYSQL_ALLOW_EMPTY_PASSWORD=1 -d mysql - - # All the required data can be loaded by running example/db_setup.sql. - # If you're using the above container, the root user has a blank password - > mysql -u root < example/db_setup.sql -``` - -Please note that this container is just for demonstrative purposes, -and is not suitable for production. - -The root MySQL user for these containers is `root` and it has an empty password. - - -[section:async_callbacks Async functions using callbacks] - -This example demonstrates how use the asynchronous functions using callbacks. - -__assume_setup__ - -[import ../../example/async_callbacks.cpp] -[example_async_callbacks] - -[endsect] - - - - -[section:async_futures Async functions using futures] - -This example demonstrates how use the asynchronous functions using futures. - -__assume_setup__ - -[import ../../example/async_futures.cpp] -[example_async_futures] - -[endsect] - - - - -[section:async_coroutines Async functions using stackful coroutines] - -This example demonstrates how use the asynchronous functions using stackful coroutines -(using [asioreflink yield_context yield_context] and -[asioreflink spawn spawn]). - -__assume_setup__ - -[import ../../example/async_coroutines.cpp] -[example_async_coroutines] - -[endsect] - - - - -[section:async_coroutinescpp20 Async functions using C++20 coroutines] - -This example demonstrates how use the asynchronous functions using C++20 coroutines -(using [asioreflink use_awaitable use_awaitable] and [asioreflink -co_spawn co_spawn]). - -__assume_setup__ - -[import ../../example/async_coroutinescpp20.cpp] -[example_async_coroutinescpp20] - -[endsect] - - - - -[section:timeouts Timeouts] - -This example demonstrates how to use Boost.Asio's -cancellation features to add timeouts to your async operations, -including the ones provided by __Self__. -For that purpose, it employs C++20 coroutines. -If you are not familiar with them, look at -[link mysql.examples.async_coroutinescpp20 this example] -first. - -__assume_setup__ - -[import ../../example/timeouts.cpp] -[example_timeouts] - -[endsect] - - - - -[section:prepared_statements_cpp11 Using prepared statements with the dynamic interface (C++11)] - -This example demonstrates how to use prepared statements with the dynamic interface to -implement a minimal order management system for an online store. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -This example requires you to run [link_to_file example/order_management/db_setup.sql]. -You can find table definitions there. - -[import ../../example/order_management/prepared_statements_cpp11.cpp] - -[example_prepared_statements_cpp11] - -[endsect] - - - - -[section:prepared_statements_cpp14 Using prepared statements with the static interface (C++14)] - -This example demonstrates how to use prepared statements with the static interface to -implement a minimal order management system for an online store. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -This example requires you to run [link_to_file example/order_management/db_setup.sql]. -You can find table definitions there. - -[import ../../example/order_management/prepared_statements_cpp14.cpp] - -[example_prepared_statements_cpp14] - -[endsect] - - - - -[section:stored_procedures_cpp11 Using stored procedures with the dynamic interface (C++11)] - -This example demonstrates how to use stored procedures with the dynamic interface to -implement a minimal order management system for an online store. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -This example requires you to run [link_to_file example/order_management/db_setup.sql]. -You can find table and procedure definitions there. - -[import ../../example/order_management/stored_procedures_cpp11.cpp] - -[example_stored_procedures_cpp11] - -[endsect] - - - - -[section:stored_procedures_cpp14 Using stored procedures with the static interface (C++14)] - -This example demonstrates how to use stored procedures with the static interface to -implement a minimal order management system for an online store. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -This example requires you to run [link_to_file example/order_management/db_setup.sql]. -You can find table and procedure definitions there. - -[import ../../example/order_management/stored_procedures_cpp14.cpp] - -[example_stored_procedures_cpp14] - -[endsect] - - - - -[section:metadata Metadata] - -This example demonstrates how to use the available metadata in a [reflink results] object. -It employs synchronous functions with exceptions as error handling. __see_error_handling__ - -__assume_setup__ - -[import ../../example/metadata.cpp] -[example_metadata] - -[endsect] - - - - -[section:unix_socket UNIX sockets] - -This example demonstrates how to establish a connection -to a MySQL server using a UNIX domain socket. The path -to the UNIX socket can be passed in as third parameter -in the command line, and defaults to `/var/run/mysqld/mysqld.sock`, -the default on most systems. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -__assume_setup__ - -[import ../../example/unix_socket.cpp] -[example_unix_socket] - -[endsect] - - - - -[section:ssl Setting SSL options] - -This example demonstrates how to configure SSL options -like server certificate verification and hostname validation. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -__assume_setup__ Additionally, you should run your MySQL server -with some test certificates we created for you, just for this example. -You can find them in this project's GitHub repository, under `tools/ssl`. -If you're using the docker container, the setup has already been done -for you. - -[import ../../example/ssl.cpp] -[example_ssl] - -[endsect] - - - - -[section:source_script Using multi-queries to source a .sql file] - -This example demonstrates how to source a .sql script using the -[link mysql.multi_resultset.multi_queries multi-queries feature]. - -Note that commands like `DELIMITER` won't work, since these are handled -by the `mysql` command line tool, rather than the server. - -The example employs synchronous functions with -exceptions as error handling. __see_error_handling__ - -__assume_setup__ - -[import ../../example/source_script.cpp] -[example_source_script] - -[endsect] - - - - - -[section:any_connection Using type-erased connections] - -This example demonstrates how to use [reflink any_connection]. - -The example employs async functions with stackful coroutines. - -__assume_setup__ - -[import ../../example/any_connection.cpp] -[example_any_connection] - -[endsect] - - - - - -[section:batch_inserts Batch inserts using client-side query formatting] - -This example demonstrates how to use client-side query formatting using -[reflink format_sql_to] to implement batch inserts. Batch inserting -can highly improve application performance. - -The example employs sync functions with exceptions. - -__assume_setup__ - -[import ../../example/batch_inserts.cpp] -[example_batch_inserts] - -[endsect] - - - - - -[section:batch_inserts_generic Generic batch inserts with Boost.Describe: extending format_sql] - -This example demonstrates how to extend [reflink format_sql] using [reflink formatter] -to implement batch inserts for any struct that contains Boost.Describe metadata. - -The example employs sync functions with exceptions. - -__assume_setup__ - -[import ../../example/batch_inserts_generic.cpp] -[example_batch_inserts_generic] - -[endsect] - - - - - - -[section:dynamic_filters Dynamic filters using client-side query formatting] - -This example demonstrates how to use [reflink format_sql_to] -to implement filters that can be enabled dynamically. - -The example employs async functions with stackful coroutines. - -__assume_setup__ - -[import ../../example/dynamic_filters.cpp] -[example_dynamic_filters] - -[endsect] - - - - - - -[section:patch_updates PATCH-like updates using client-side query formatting] - -This example demonstrates how use [reflink format_sql_to] -to implement `UPDATE` queries with `PATCH` semantics, i.e., -that update a dynamic set of fields. - -The example employs sync functions with exceptions. - -__assume_setup__ - -[import ../../example/patch_updates.cpp] -[example_patch_updates] - -[endsect] - - - - - - - - - -[section:pipeline (Experimental) Pipelines] - -This example demonstrates how use [link mysql.pipeline the pipeline API] -to prepare, execute and close statements in batch. - -The example employs async functions with C++20 coroutines. - -__assume_setup__ - -[import ../../example/pipeline.cpp] -[example_pipeline] - -[endsect] - - - - - - - - -[section:connection_pool Connection pools] - -This example demonstrates how to use [reflink connection_pool]. -It implements an HTTP REST API server for a text notes -application. The API provides CRUD methods on note objects. -Instead of opening a new MySQL connection per HTTP request, -the server uses a connection pool to reuse connections. - -The example employs async functions with stackful coroutines. - -This example contains multiple files, and requires linking to -__Context__, [@boost:/libs/json/index.html Boost.Json] and -and [@boost:/libs/url/index.html Boost.Url]. -__assume_setup__ - -[import ../../example/connection_pool/main.cpp] -[example_connection_pool_main_cpp] - -[import ../../example/connection_pool/types.hpp] -[example_connection_pool_types_hpp] - -[import ../../example/connection_pool/repository.hpp] -[example_connection_pool_repository_hpp] - -[import ../../example/connection_pool/repository.cpp] -[example_connection_pool_repository_cpp] - -[import ../../example/connection_pool/handle_request.hpp] -[example_connection_pool_handle_request_hpp] - -[import ../../example/connection_pool/handle_request.cpp] -[example_connection_pool_handle_request_cpp] - -[import ../../example/connection_pool/server.hpp] -[example_connection_pool_server_hpp] - -[import ../../example/connection_pool/server.cpp] -[example_connection_pool_server_cpp] - -[import ../../example/connection_pool/log_error.hpp] -[example_connection_pool_log_error_hpp] - - -[endsect] - - - - - -[endsect] [/ examples] \ No newline at end of file diff --git a/doc/qbk/helpers/Formattable.qbk b/doc/qbk/helpers/Formattable.qbk index 4b6df1b8f..027521079 100644 --- a/doc/qbk/helpers/Formattable.qbk +++ b/doc/qbk/helpers/Formattable.qbk @@ -25,6 +25,7 @@ Formally, let `T` be any type, and `U` the result of stripping cv-qualifiers and formatted as a blob, not as a sequence). * `U` is [reflink formattable_ref]. -For a reference table on built-in formattable types, see [link mysql.sql_formatting.reference this section]. +For a reference table on built-in formattable types, see +[link mysql.sql_formatting_advanced.reference this section]. [endsect] \ No newline at end of file diff --git a/doc/qbk/helpers/quickref.xml b/doc/qbk/helpers/quickref.xml index 7d74c6293..791a5d29d 100644 --- a/doc/qbk/helpers/quickref.xml +++ b/doc/qbk/helpers/quickref.xml @@ -146,7 +146,7 @@ Dynamic interface type mappings ReadableField types WritableField types - Formattable types + Formattable types Pipeline stage reference (experimental) String encoding diff --git a/example/tutorial.cpp b/example/1_tutorial/1_sync.cpp similarity index 54% rename from example/tutorial.cpp rename to example/1_tutorial/1_sync.cpp index f0f2f5bb0..19e13d33a 100644 --- a/example/tutorial.cpp +++ b/example/1_tutorial/1_sync.cpp @@ -5,28 +5,28 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//[tutorial_listing +//[example_tutorial_sync +/** + * Creates a connection, establishes a session and + * runs a simple "Hello world!" query. + * + * This example uses synchronous functions and handles errors using exceptions. + */ + +#include +#include #include -#include #include -#include #include -#include -#include -#include #include -#include -/** - * For this example, we will be using the 'boost_mysql_examples' database. - * You can get this database by running db_setup.sql. - * This example assumes you are connecting to a localhost MySQL server. - * - * This example uses synchronous functions and handles errors using exceptions. - */ +//[tutorial_sync_namespaces +namespace mysql = boost::mysql; +namespace asio = boost::asio; +//] void main_impl(int argc, char** argv) { @@ -36,50 +36,47 @@ void main_impl(int argc, char** argv) exit(1); } - //[tutorial_connection - // The execution context, required to run I/O operations. - boost::asio::io_context ctx; + const char* hostname = argv[3]; + const char* username = argv[1]; + const char* password = argv[2]; - // The SSL context, required to establish TLS connections. - // The default SSL options are good enough for us at this point. - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); + //[tutorial_sync_connection + // The execution context, required to run I/O operations. + asio::io_context ctx; // Represents a connection to the MySQL server. - boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); + mysql::any_connection conn(ctx); //] - //[tutorial_connect - // Resolve the hostname to get a collection of endpoints - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - auto endpoints = resolver.resolve(argv[3], boost::mysql::default_port_string); - - // The username, password and database to use - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database - ); + //[tutorial_sync_main + //[tutorial_sync_connect + // The hostname, username and password to use + mysql::connect_params params; + params.server_address.emplace_host_and_port(hostname); + params.username = username; + params.password = password; - // Connect to the server using the first endpoint returned by the resolver - conn.connect(*endpoints.begin(), params); + // Connect to the server + conn.connect(params); //] - //[tutorial_query + //[tutorial_sync_query // Issue the SQL query to the server const char* sql = "SELECT 'Hello world!'"; - boost::mysql::results result; + mysql::results result; conn.execute(sql, result); //] - //[tutorial_results + //[tutorial_sync_results // Print the first field in the first row std::cout << result.rows().at(0).at(0) << std::endl; //] - //[tutorial_close + //[tutorial_sync_close // Close the connection conn.close(); //] + //] } int main(int argc, char** argv) @@ -88,7 +85,7 @@ int main(int argc, char** argv) { main_impl(argc, argv); } - catch (const boost::mysql::error_with_diagnostics& err) + catch (const 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 diff --git a/example/1_tutorial/2_async.cpp b/example/1_tutorial/2_async.cpp new file mode 100644 index 000000000..a9b06c89c --- /dev/null +++ b/example/1_tutorial/2_async.cpp @@ -0,0 +1,155 @@ +// +// 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_tutorial_async + +/** + * This example is analogous to the synchronous tutorial, but uses async functions + * with C++20 coroutines, instead. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +/** + * The main coroutine. + * It must have a return type of asio::awaitable. + * Our coroutine does not communicate any result back, so T=void. + * + * The coroutine will suspend every time we call one of the asynchronous functions, saving + * all information it needs for resuming. When the asynchronous operation completes, + * the coroutine will resume in the point it was left. + * We use the same program structure as in the sync world, replacing + * sync functions by their async equivalents and adding co_await in front of them. + */ +//[tutorial_async_coro +asio::awaitable coro_main( + mysql::any_connection& conn, + std::string_view server_hostname, + std::string_view username, + std::string_view password +) +{ + // The hostname, username, password and database to use. + // TLS is used by default. + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; + + // Connect to the server + co_await conn.async_connect(params); + + // Issue the SQL query to the server + const char* sql = "SELECT 'Hello world!'"; + mysql::results result; + co_await conn.async_execute(sql, result); + + // Print the first field in the first row + std::cout << result.rows().at(0).at(0) << std::endl; + + // Close the connection + co_await conn.async_close(); +} +//] + +void main_impl(int argc, char** argv) +{ + if (argc != 4) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + //[tutorial_async_connection + // The execution context, required to run I/O operations. + asio::io_context ctx; + + // Represents a connection to the MySQL server. + mysql::any_connection conn(ctx); + //] + + //[tutorial_async_co_spawn + // Enqueue the coroutine for execution. + // This does not wait for the coroutine to finish. + asio::co_spawn( + // The execution context where the coroutine will run + ctx, + + // The coroutine to run. This must be a function taking no arguments + // and returning an asio::awaitable + [&conn, argv] { return coro_main(conn, argv[3], argv[1], argv[2]); }, + + // Callback to run when the coroutine completes. + // If any exception is thrown in the coroutine body, propagate it to terminate the program. + [](std::exception_ptr ptr) { + if (ptr) + { + std::rethrow_exception(ptr); + } + } + ); + //] + + //[tutorial_async_run + // Calling run will actually execute the coroutine until completion + ctx.run(); + //] +} + +int main(int argc, char** argv) +{ + try + { + main_impl(argc, argv); + } + catch (const 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() << '\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 \ No newline at end of file diff --git a/example/1_tutorial/3_with_params.cpp b/example/1_tutorial/3_with_params.cpp new file mode 100644 index 000000000..7fd158c95 --- /dev/null +++ b/example/1_tutorial/3_with_params.cpp @@ -0,0 +1,168 @@ +// +// 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_tutorial_with_params + +/** + * This example shows how to issue queries with parameters containing + * untrusted input securely. Given an employee ID, it prints their full name. + * The example builds on the previous async tutorial. + * + * 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 + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +//[tutorial_with_params_coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password, + std::int64_t employee_id +) +{ + //[tutorial_with_params_connection + // The connection will use the same executor as the coroutine + mysql::any_connection conn(co_await asio::this_coro::executor); + //] + + //[tutorial_with_params_connect_params + // The hostname, 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.database = "boost_mysql_examples"; + //] + + // Connect to the server + co_await conn.async_connect(params); + + //[tutorial_with_params_execute + // Execute the query with the given parameters. When executed, with_params + // expands the given query string template and sends it to the server for execution. + // {} are placeholders, as in std::format. Values are escaped as required to prevent + // SQL injection. + mysql::results result; + co_await conn.async_execute( + mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id), + result + ); + //] + + //[tutorial_with_params_results + // Did we find an employee with that ID? + if (result.rows().empty()) + { + std::cout << "Employee not found" << std::endl; + } + else + { + // Print the retrieved details. The first field is the first name, + // and the second, the last name. + mysql::row_view employee = result.rows().at(0); + std::cout << "Employee's name is: " << employee.at(0) << ' ' << employee.at(1) << std::endl; + } + //] + + // Close the connection + co_await conn.async_close(); +} +//] + +void main_impl(int argc, char** argv) +{ + if (argc != 5) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + // The execution context, required to run I/O operations. + asio::io_context ctx; + + // Enqueue the coroutine for execution. + asio::co_spawn( + // The execution context where the coroutine will run + ctx, + + // The coroutine to run. This must be a function taking no arguments + // and returning an asio::awaitable + [argv] { return coro_main(argv[3], argv[1], argv[2], std::stoi(argv[4])); }, + + // Callback to run when the coroutine completes. + // If any exception is thrown in the coroutine body, propagate it to terminate the program. + [](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 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() << '\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 \ No newline at end of file diff --git a/example/1_tutorial/4_static_interface.cpp b/example/1_tutorial/4_static_interface.cpp new file mode 100644 index 000000000..6308ee8cc --- /dev/null +++ b/example/1_tutorial/4_static_interface.cpp @@ -0,0 +1,187 @@ +// +// 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_static_interface + +/** + * This example shows how to use the static interface to parse + * the results of a query into a C++ struct. + * Like the previous tutorial, given an employee ID, + * it prints their full name. + * + * 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 + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +//[tutorial_static_fn +void print_employee(std::string_view first_name, std::string_view last_name) +{ + std::cout << "Employee's name is: " << first_name << ' ' << last_name << std::endl; +} +//] + +//[tutorial_static_struct +// Should contain a member for each field of interest present in our query. +// Declaration order doesn't need to match field order in the query. +// Field names should match the ones in our query +struct employee +{ + std::string first_name; + std::string last_name; +}; +//] + +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password, + std::int64_t employee_id +) +{ + // Represents a connection to the MySQL server. + // The 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. + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; + params.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + //[tutorial_static_execute + // Using static_results will parse the result of our query + // into instances of the employee type. Fields will be matched + // by name, instead of by position. + // pfr_by_name tells the library to use Boost.Pfr for reflection, + // and to match fields by name. + mysql::static_results> result; + + // Execute the query with the given parameters, performing the required + // escaping to prevent SQL injection. + co_await conn.async_execute( + mysql::with_params("SELECT first_name, last_name FROM employee WHERE id = {}", employee_id), + result + ); + //] + + //[tutorial_static_results + // Did we find an employee with that ID? + if (result.rows().empty()) + { + std::cout << "Employee not found" << std::endl; + } + else + { + // Print the retrieved details + const employee& emp = result.rows()[0]; + print_employee(emp.first_name, emp.last_name); + } + //] + + // Close the connection + co_await conn.async_close(); +} + +void main_impl(int argc, char** argv) +{ + if (argc != 5) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + // The execution context, required to run I/O operations. + asio::io_context ctx; + + // Enqueue the coroutine for execution. + asio::co_spawn( + // The execution context where the coroutine will run + ctx, + + // The coroutine to run. This must be a function taking no arguments + // and returning an asio::awaitable + [argv] { return coro_main(argv[3], argv[1], argv[2], std::stoi(argv[4])); }, + + // Callback to run when the coroutine completes. + // If any exception is thrown in the coroutine body, propagate it to terminate the program. + [](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 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() << '\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 \ No newline at end of file diff --git a/example/batch_inserts.cpp b/example/2_simple/batch_inserts.cpp similarity index 64% rename from example/batch_inserts.cpp rename to example/2_simple/batch_inserts.cpp index aaff4e2d6..ec3d356ef 100644 --- a/example/batch_inserts.cpp +++ b/example/2_simple/batch_inserts.cpp @@ -5,29 +5,41 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include -#ifdef BOOST_DESCRIBE_CXX14 +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT //[example_batch_inserts -// Uses client-side SQL formatting to implement batch inserts -// for a specific type. -// The program reads a JSON file containing a list of employees -// and inserts it into the employee table. -// -// This example requires C++14 to work because it uses Boost.Describe -// to simplify JSON parsing. All Boost.MySQL features used are C++11 compatible. +/** + * This example demonstrates how to insert several records in a single + * SQL statement using format_sql. + * + * The program reads a JSON file containing a list of employees + * and inserts it into the employee table. It uses Boost.JSON and + * Boost.Describe to parse the file. + * + * This example uses C++20 coroutines. If you need, you can backport + * it to C++14 (required by Boost.Describe) 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 @@ -35,6 +47,10 @@ #include #include +namespace asio = boost::asio; +namespace mysql = boost::mysql; +namespace json = boost::json; + /** * We will use Boost.Describe to easily parse the JSON file * into a std::vector. The JSON file contain an array @@ -55,6 +71,7 @@ struct employee }; // Adds reflection capabilities to employee. Required by the JSON parser. +// Boost.Describe requires C++14 BOOST_DESCRIBE_STRUCT(employee, (), (first_name, last_name, company_id, salary)) // Reads a file into memory @@ -66,58 +83,37 @@ static std::string read_file(const char* file_name) return std::string(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); } -void main_impl(int argc, char** argv) +// The main coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password, + const std::vector& employees +) { - if (argc != 5) - { - std::cerr << "Usage: " << argv[0] << " \n"; - exit(1); - } - - // Read our JSON file into memory - auto contents = read_file(argv[4]); - - // Parse the JSON. json::parse parses the string into a DOM, - // and json::value_to validates the JSON schema, parsing values into employee structures - auto values = boost::json::value_to>(boost::json::parse(contents)); - - // We need one employee, at least - if (values.empty()) - { - std::cerr << "Input file should contain one employee, at least\n"; - exit(1); - } - - // Create an I/O context, required by all I/O objects - boost::asio::io_context ctx; - - // Create a connection. Note that client-side SQL formatting - // requires us to use the newer any_connection. - boost::mysql::any_connection conn(ctx); - - // Connection configuration. By default, connections use the utf8mb4 character set - // (MySQL's name for regular UTF-8). - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(argv[3]); - params.username = argv[1]; - params.password = argv[2]; + // 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 + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; params.database = "boost_mysql_examples"; - // A results object to hold the result of executing our SQL query - boost::mysql::results result; - // Connect to the server - conn.connect(params); + co_await conn.async_connect(params); // A function describing how to format a single employee object. Used with mysql::sequence. - auto format_employee_fn = [](const employee& emp, boost::mysql::format_context_base& ctx) { + auto format_employee_fn = [](const employee& emp, mysql::format_context_base& ctx) { // format_context_base can be used to build query strings incrementally. // Used internally by the sequence() formatter. // format_sql_to expands a format string, replacing {} fields, // and appends the result to the passed context. // When formatted, strings are quoted and escaped as string literals. // ints are formatted as number literals. - boost::mysql::format_sql_to( + mysql::format_sql_to( ctx, "({}, {}, {}, {})", emp.first_name, @@ -132,19 +128,61 @@ void main_impl(int argc, char** argv) // When inserting two employees, something like the following may be generated: // INSERT INTO employee (first_name, last_name, company_id, salary) // VALUES ('John', 'Doe', 'HGS', 20000), ('Rick', 'Smith', 'LLC', 50000) - // By default, sequence copies the input range, but we don't need this here, - // so we disable the copy by calling ref() - conn.execute( - boost::mysql::with_params( + mysql::results result; + co_await conn.async_execute( + mysql::with_params( "INSERT INTO employee (first_name, last_name, company_id, salary) VALUES {}", - boost::mysql::sequence(std::ref(values), format_employee_fn) + mysql::sequence(std::ref(employees), format_employee_fn) ), result ); - std::cout << "Done\n"; // Notify the MySQL server we want to quit, then close the underlying connection. - conn.close(); + co_await conn.async_close(); +} + +void main_impl(int argc, char** argv) +{ + if (argc != 5) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + // Read our JSON file into memory + auto contents = read_file(argv[4]); + + // Parse the JSON. json::parse parses the string into a DOM, + // and json::value_to validates the JSON schema, parsing values into employee structures + auto values = json::value_to>(json::parse(contents)); + + // We need one employee, at least + if (values.empty()) + { + std::cerr << "Input file should contain one employee, at least\n"; + exit(1); + } + + // 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], values); }, + // 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) @@ -153,7 +191,7 @@ int main(int argc, char** argv) { main_impl(argc, argv); } - catch (const boost::mysql::error_with_diagnostics& err) + catch (const 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 diff --git a/example/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp similarity index 65% rename from example/batch_inserts_generic.cpp rename to example/2_simple/batch_inserts_generic.cpp index 4e8952d25..fa8875538 100644 --- a/example/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -5,28 +5,37 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include - -#ifdef BOOST_DESCRIBE_CXX14 +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT //[example_batch_inserts_generic -// Uses client-side SQL formatting to implement batch inserts -// for any type T with Boost.Describe metadata. -// -// The program reads a JSON file containing a list of employees -// and inserts it into the employee table. -// -// This example requires C++14 to work. +/** + * This example demonstrates how to insert several records in a single + * SQL statement using format_sql. The implementation is generic, + * and can be reused to batch-insert any type T with Boost.Describe metadata. + * + * The program reads a JSON file containing a list of employees + * and inserts it into the employee table. It uses Boost.JSON and + * Boost.Describe to parse the file. + * + * This example uses C++20 coroutines. If you need, you can backport + * it to C++14 (required by Boost.Describe) 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 @@ -41,10 +50,13 @@ #include #include #include +#include -using boost::mysql::string_view; namespace describe = boost::describe; namespace mp11 = boost::mp11; +namespace mysql = boost::mysql; +namespace asio = boost::asio; +namespace json = boost::json; /** * An example Boost.Describe struct. Our code will work with any struct like this, @@ -73,11 +85,11 @@ constexpr std::size_t num_public_members = mp11::mp_size>::val // For employee, generates // {"first_name", "last_name", "company_id", "salary"} template -constexpr std::array> get_field_names() +constexpr std::array> get_field_names() { return mp11::tuple_apply( [](auto... descriptors) { - return std::array>{{descriptors.name...}}; + return std::array>{{descriptors.name...}}; }, mp11::mp_rename, std::tuple>() ); @@ -90,13 +102,13 @@ constexpr std::array> get_field_names() struct insert_struct_format_fn { template - void operator()(const T& value, boost::mysql::format_context_base& ctx) const + void operator()(const T& value, mysql::format_context_base& ctx) const { // Convert the struct into a std::array of formattable_ref // formattable_ref is a view type that can hold any type that can be formatted auto args = mp11::tuple_apply( [&value](auto... descriptors) { - return std::array>{ + return std::array>{ {value.*descriptors.pointer...} }; }, @@ -104,7 +116,7 @@ struct insert_struct_format_fn ); // Format them as a comma-separated sequence - boost::mysql::format_sql_to(ctx, "({})", args); + mysql::format_sql_to(ctx, "({})", args); } }; @@ -117,6 +129,44 @@ std::string read_file(const char* file_name) return std::string(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); } +// The main coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password, + const std::vector& employees +) +{ + // 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 + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; + params.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + // Run the query. Placeholders ({}) will be expanded before the query is sent to the server. + // We use sequence() to format C++ ranges as comma-separated sequences. + mysql::results result; + co_await conn.async_execute( + mysql::with_params( + "INSERT INTO employee ({::i}) VALUES {}", + get_field_names(), + mysql::sequence(std::ref(employees), insert_struct_format_fn()) + ), + result + ); + + // 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 != 5) @@ -130,25 +180,7 @@ void main_impl(int argc, char** argv) // Parse the JSON. json::parse parses the string into a DOM, // and json::value_to validates the JSON schema, parsing values into employee structures - auto values = boost::json::value_to>(boost::json::parse(contents)); - - // Create an I/O context, required by all I/O objects - boost::asio::io_context ctx; - - // Create a connection. Note that client-side SQL formatting - // requires us to use the newer any_connection. - boost::mysql::any_connection conn(ctx); - - // Connection configuration. By default, connections use the utf8mb4 character set - // (MySQL's name for regular UTF-8). - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(argv[3]); - params.username = argv[1]; - params.password = argv[2]; - params.database = "boost_mysql_examples"; - - // Connect to the server - conn.connect(params); + auto values = json::value_to>(json::parse(contents)); // We need one value to insert, at least if (values.empty()) @@ -157,23 +189,26 @@ void main_impl(int argc, char** argv) exit(1); } - // Run the query. Placeholders ({}) will be expanded before the query is sent to the server. - // We use sequence() to format C++ ranges as comma-separated sequences. - // By default, sequence copies its input range, but we don't need this here, - // so we disable the copy by calling ref() - boost::mysql::results result; - conn.execute( - boost::mysql::with_params( - "INSERT INTO employee ({::i}) VALUES {}", - get_field_names(), - boost::mysql::sequence(std::ref(values), insert_struct_format_fn()) - ), - result + // 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], values); }, + // If any exception is thrown in the coroutine body, rethrow it. + [](std::exception_ptr ptr) { + if (ptr) + { + std::rethrow_exception(ptr); + } + } ); - std::cout << "Done\n"; - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.close(); + // Calling run will actually execute the coroutine until completion + ctx.run(); + + std::cout << "Done\n"; } int main(int argc, char** argv) @@ -182,7 +217,7 @@ int main(int argc, char** argv) { main_impl(argc, argv); } - catch (const boost::mysql::error_with_diagnostics& err) + catch (const 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 diff --git a/example/2_simple/callbacks.cpp b/example/2_simple/callbacks.cpp new file mode 100644 index 000000000..c165f833c --- /dev/null +++ b/example/2_simple/callbacks.cpp @@ -0,0 +1,178 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +//[example_callbacks + +/** + * This example demonstrates how to use callbacks when using async functions. + * This can be a good choice when targeting a standard lower than C++20. + * 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 + +// When using callbacks, we usually employ error codes instead of exceptions. +using boost::system::error_code; + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +// Prints a database employee to stdout +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) +} + +// A session object, containing all variables that need to be kept alive for our session. +// We will use a shared_ptr to ensure that all these variables are kept alive +// until the last callback is executed +class session : public std::enable_shared_from_this +{ + mysql::connect_params conn_params; // MySQL credentials and other connection config + mysql::any_connection conn; // Represents the connection to the MySQL server + mysql::results result; // A result from a query + mysql::error_code final_error; // Will be set in case of error + mysql::diagnostics diag; // Will be populated with info about server errors + const char* company_id; // The ID of the company whose employees we want to list. Untrusted. +public: + session( + asio::io_context& ctx, + const char* server_hostname, + const char* username, + const char* password, + const char* company_id + ) + : conn(ctx), company_id(company_id) + { + conn_params.server_address.emplace_host_and_port(server_hostname); + conn_params.username = username; + conn_params.password = password; + conn_params.database = "boost_mysql_examples"; + } + + // Accessor for error information, so main can access it + error_code get_error() const { return final_error; } + const boost::mysql::diagnostics& get_diagnostics() const { return diag; } + + // Initiates the callback chain + void start() + { + // Will call on_connect when the connect operation completes. + // The session object is kept alive with the shared_ptr that shared_from_this produces + conn.async_connect( + conn_params, + diag, + std::bind(&session::on_connect, shared_from_this(), std::placeholders::_1) + ); + } + + void on_connect(error_code ec) + { + // If there was an error, stop the callback chain + if (ec) + { + final_error = ec; + return; + } + + // Initiate the query execution. company_id is an untrusted value. + // with_params will securely compose a SQL query and send it to the server for execution. + // Returned rows will be read into result. + // We use the callback chain + shared_ptr technique again + conn.async_execute( + mysql::with_params( + "SELECT first_name, last_name, salary FROM employee WHERE company_id = {}", + company_id + ), + result, + diag, + std::bind(&session::on_execute, shared_from_this(), std::placeholders::_1) + ); + } + + void on_execute(error_code ec) + { + // If there was an error, stop the callback chain + if (ec) + { + final_error = ec; + return; + } + + // Print the rows returned by the query + for (boost::mysql::row_view employee : result.rows()) + { + print_employee(employee); + } + + // Notify the MySQL server we want to quit and then close the socket + conn.async_close(diag, std::bind(&session::finish, shared_from_this(), std::placeholders::_1)); + } + + void finish(error_code err) { final_error = err; } +}; + +void main_impl(int argc, char** argv) +{ + if (argc != 4 && argc != 5) + { + std::cerr << "Usage: " << argv[0] << " [company-id]\n"; + exit(1); + } + + // The execution context, required to run I/O operations. + boost::asio::io_context ctx; + + // The company_id whose employees we will be listing. This + // is user-supplied input, and should be treated as untrusted. + const char* company_id = argc == 5 ? argv[4] : "HGS"; + + // Create the session object and launch it + auto sess = std::make_shared(ctx, argv[3], argv[1], argv[2], company_id); + sess->start(); + + // Run the callback chain until it completes + ctx.run(); + + // Check for errors + if (error_code ec = sess->get_error()) + { + std::cerr << "Error: " << ec << ": " << ec.message() << '\n' + << "Server diagnostics: " << sess->get_diagnostics().server_message() << std::endl; + exit(1); + } +} + +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; + } +} + +//] diff --git a/example/2_simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp new file mode 100644 index 000000000..63b7d87cb --- /dev/null +++ b/example/2_simple/coroutines_cpp11.cpp @@ -0,0 +1,154 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +//[example_coroutines_cpp11 + +/** + * This example demonstrates how to use stackful coroutines when using async functions. + * This can be a good choice when targeting a standard lower than C++20. + * This example uses the 'boost_mysql_examples' database, which you + * can get by running db_setup.sql. + * You need to link your program to Boost.Context to use asio::spawn. + * + * 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 + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +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) +} + +/** + * The main coroutine. It will suspend every time we call one of the asynchronous functions, saving + * all information it needs for resuming. When the asynchronous operation completes, + * the coroutine will resume in the point it was left. + * We need to pass the yield object to async functions for this to work. + */ +void coro_main( + const char* server_hostname, + const char* username, + const char* password, + const char* company_id, + asio::yield_context yield +) +{ + // Represents a connection to the MySQL server. + // The connection will use the same executor as the coroutine + mysql::any_connection conn(yield.get_executor()); + + // The hostname, username, password and database to use + mysql::connect_params conn_params; + conn_params.server_address.emplace_host_and_port(server_hostname); + conn_params.username = username; + conn_params.password = password; + conn_params.database = "boost_mysql_examples"; + + // Connect to server. with_diagnostics turns thrown exceptions + // into error_with_diagnostics, which contain more info than regular exceptions + conn.async_connect(conn_params, mysql::with_diagnostics(yield)); + + // Initiate the query execution. company_id is an untrusted value. + // with_params will securely compose a SQL query and send it to the server for execution. + // Returned rows will be read into result. + mysql::results result; + conn.async_execute( + mysql::with_params( + "SELECT first_name, last_name, salary FROM employee WHERE company_id = {}", + company_id + ), + result, + mysql::with_diagnostics(yield) + ); + + // Print the employees + for (boost::mysql::row_view employee : result.rows()) + { + print_employee(employee); + } + + // Notify the MySQL server we want to quit, then close the underlying connection. + conn.async_close(mysql::with_diagnostics(yield)); +} + +void main_impl(int argc, char** argv) +{ + if (argc != 4 && argc != 5) + { + std::cerr << "Usage: " << argv[0] << " [company-id]\n"; + exit(1); + } + + // The company_id whose employees we will be listing. This + // is user-supplied input, and should be treated as untrusted. + const char* company_id = argc == 5 ? argv[4] : "HGS"; + + // The execution context, required to run I/O operations. + asio::io_context ctx; + + // Launch the coroutine + asio::spawn( + ctx, + [argv, company_id](asio::yield_context yield) { + coro_main(argv[3], argv[1], argv[2], company_id, yield); + }, + // 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) + { + // You will only get this type of exceptions if you use with_diagnostics. + // Some errors include additional diagnostics, like server-provided error messages. + // Security note: diagnostics::server_message may contain user-supplied values (e.g. the + // field value that caused the error) and is encoded using to the connection's character set + // (UTF-8 by default). Treat is as untrusted input. + std::cerr << "Error: " << err.what() << '\n' + << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; + return 1; + } + catch (const std::exception& err) + { + std::cerr << "Error: " << err.what() << std::endl; + return 1; + } +} + +//] diff --git a/example/2_simple/disable_tls.cpp b/example/2_simple/disable_tls.cpp new file mode 100644 index 000000000..87fbf3049 --- /dev/null +++ b/example/2_simple/disable_tls.cpp @@ -0,0 +1,139 @@ +// +// 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_disable_tls + +/** + * This example demonstrates how to disable TLS when connecting to MySQL. + * + * 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 + +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 +) +{ + // Create a connection. + // Will use the same executor as the coroutine. + mysql::any_connection conn(co_await asio::this_coro::executor); + + //[section_connection_establishment_disable_tls + // The server host, username, password and database to use. + // Passing ssl_mode::disable will disable the use of TLS. + 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"; + params.ssl = mysql::ssl_mode::disable; + //] + + // Connect to the server + co_await conn.async_connect(params); + + // The connection can now be used normally + mysql::results result; + co_await conn.async_execute("SELECT 'Hello world!'", result); + std::cout << result.rows().at(0).at(0) << 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 != 4) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2]); }, + // 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/dynamic_filters.cpp b/example/2_simple/dynamic_filters.cpp similarity index 62% rename from example/dynamic_filters.cpp rename to example/2_simple/dynamic_filters.cpp index b6828f74e..8399ba81b 100644 --- a/example/dynamic_filters.cpp +++ b/example/2_simple/dynamic_filters.cpp @@ -5,11 +5,23 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT + //[example_dynamic_filters -// Uses client-side SQL formatting to implement a dynamic filter. -// If you're implementing a filter with many options that can be -// conditionally enabled, this pattern may be useful for you. +/** + * This example implements a dynamic filter using client-side SQL. + * If you're implementing a filter with many options that can be + * conditionally enabled, this pattern may be useful for you. + * + * This example 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 @@ -19,26 +31,24 @@ #include #include #include -#include -#include -#include +#include +#include #include -#include -#include #include #include #include +#include #include +#include #include -using boost::mysql::field_view; -using boost::mysql::string_view; -using boost::mysql::with_diagnostics; +namespace mysql = boost::mysql; +namespace asio = boost::asio; // Prints an employee row to stdout -void print_employee(boost::mysql::row_view employee) +void print_employee(mysql::row_view employee) { std::cout << "id: " << employee.at(0) // field 0: id << ", first_name: " << std::setw(16) << employee.at(1) // field 1: first_name @@ -58,7 +68,7 @@ enum class op_type }; // Returns the SQL operator for the given op_type -string_view op_type_to_sql(op_type value) +std::string_view op_type_to_sql(op_type value) { switch (value) { @@ -76,39 +86,39 @@ string_view op_type_to_sql(op_type value) // `salary` > 20000 condition struct filter { - string_view field_name; // The database column name - op_type op; // The operator to apply - field_view field_value; // The value to check. field_view can hold any MySQL type + std::string_view field_name; // The database column name + op_type op; // The operator to apply + mysql::field_view field_value; // The value to check. field_view can hold any MySQL type }; // Command line arguments struct cmdline_args { // MySQL username to use during authentication. - string_view username; + std::string_view username; // MySQL password to use during authentication. - string_view password; + std::string_view password; // Hostname where the MySQL server is listening. - string_view server_hostname; + std::string_view server_hostname; // The filters to apply std::vector filts; // If order_by.has_value(), order employees using the given field - boost::optional order_by; + std::optional order_by; }; // Parses the command line static cmdline_args parse_cmdline_args(int argc, char** argv) { // Available options - const string_view company_id_prefix = "--company-id="; - const string_view first_name_prefix = "--first-name="; - const string_view last_name_prefix = "--last-name="; - const string_view min_salary_prefix = "--min-salary="; - const string_view order_by_prefix = "--order-by="; + constexpr std::string_view company_id_prefix = "--company-id="; + constexpr std::string_view first_name_prefix = "--first-name="; + constexpr std::string_view last_name_prefix = "--last-name="; + constexpr std::string_view min_salary_prefix = "--min-salary="; + constexpr std::string_view order_by_prefix = "--order-by="; // Helper function to print the usage message and exit auto print_usage_and_exit = [argv]() { @@ -129,28 +139,28 @@ static cmdline_args parse_cmdline_args(int argc, char** argv) // Parse the filters for (int i = 4; i < argc; ++i) { - string_view arg = argv[i]; + std::string_view arg = argv[i]; // Attempt to match the argument against each prefix if (arg.starts_with(company_id_prefix)) { auto value = arg.substr(company_id_prefix.size()); - res.filts.push_back({"company_id", op_type::eq, field_view(value)}); + res.filts.push_back({"company_id", op_type::eq, mysql::field_view(value)}); } else if (arg.starts_with(first_name_prefix)) { auto value = arg.substr(first_name_prefix.size()); - res.filts.push_back({"first_name", op_type::eq, field_view(value)}); + res.filts.push_back({"first_name", op_type::eq, mysql::field_view(value)}); } else if (arg.starts_with(last_name_prefix)) { auto value = arg.substr(last_name_prefix.size()); - res.filts.push_back({"last_name", op_type::eq, field_view(value)}); + res.filts.push_back({"last_name", op_type::eq, mysql::field_view(value)}); } else if (arg.starts_with(min_salary_prefix)) { - auto value = std::stod(arg.substr(min_salary_prefix.size())); - res.filts.push_back({"salary", op_type::gte, field_view(value)}); + auto value = std::stod(std::string(arg.substr(min_salary_prefix.size()))); + res.filts.push_back({"salary", op_type::gte, mysql::field_view(value)}); } else if (arg.starts_with(order_by_prefix)) { @@ -190,20 +200,20 @@ static cmdline_args parse_cmdline_args(int argc, char** argv) // options like the current character set. Use any_connection::format_opts to obtain it. // If your use case allows you to express your query as a single format string, use with_params, instead. std::string compose_get_employees_query( - boost::mysql::format_options opts, + mysql::format_options opts, const std::vector& filts, - boost::optional order_by + std::optional order_by ) { // A format context allows composing queries incrementally. // This is required because we need to add the ORDER BY clause conditionally - boost::mysql::format_context ctx(opts); + mysql::format_context ctx(opts); // Adds an individual filter to the context. Used by sequence() - auto filter_format_fn = [](filter item, boost::mysql::format_context_base& elm_ctx) { + auto filter_format_fn = [](filter item, mysql::format_context_base& elm_ctx) { // {:i} formats a string as a SQL identifier. {:r} outputs raw SQL. // filter{"key", op_type::eq, field_view(42)} would get formatted as "`key` = 42" - boost::mysql::format_sql_to( + mysql::format_sql_to( elm_ctx, "{:i} {:r} {}", item.field_name, @@ -217,10 +227,10 @@ std::string compose_get_employees_query( // using the string " AND " as glue, to separate filters // By default, sequence copies its input range, but we don't need this here, // so we disable the copy by calling ref() - boost::mysql::format_sql_to( + mysql::format_sql_to( ctx, "SELECT id, first_name, last_name, company_id, salary FROM employee WHERE {}", - boost::mysql::sequence(std::ref(filts), filter_format_fn, " AND ") + mysql::sequence(std::ref(filts), filter_format_fn, " AND ") ); // Add the order by @@ -228,7 +238,7 @@ std::string compose_get_employees_query( { // identifier formats a string as a SQL identifier, instead of a string literal. // For instance, this may generate "ORDER BY `first_name`" - boost::mysql::format_sql_to(ctx, " ORDER BY {:i}", *order_by); + mysql::format_sql_to(ctx, " ORDER BY {:i}", *order_by); } // Get our generated query. get() returns a system::result, which @@ -238,64 +248,58 @@ std::string compose_get_employees_query( return std::move(ctx).get().value(); } +// The main coroutine +asio::awaitable coro_main(const cmdline_args& args) +{ + // 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 + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(args.server_hostname)); + params.username = args.username; + params.password = args.password; + params.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + // Compose the query. format_opts() returns a system::result, + // containing the options required by format_context. format_opts() may return + // an error if the connection doesn't know which character set is using - + // use async_set_character_set if this happens. + std::string query = compose_get_employees_query(conn.format_opts().value(), args.filts, args.order_by); + + // Execute the query as usual. Note that the query was generated + // client-side. Appropriately using format_sql_to makes this approach secure. + // with_params uses this same technique under the hood. + // Casting to string_view saves a copy in async_execute + mysql::results result; + co_await conn.async_execute(std::string_view(query), result); + + // Print the employees + for (mysql::row_view employee : result.rows()) + { + print_employee(employee); + } + + // 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) { // Parse the command line cmdline_args args = parse_cmdline_args(argc, argv); // Create an I/O context, required by all I/O objects - boost::asio::io_context ctx; - - /** - * Spawn a stackful coroutine using boost::asio::spawn. - * The coroutine suspends every time we call an asynchronous function, and - * will resume when it completes. - * Note that client-side SQL formatting can be used with both sync and async functions. - */ - boost::asio::spawn( - ctx.get_executor(), - [args](boost::asio::yield_context yield) { - // Create a connection. Note that client-side SQL formatting - // requires us to use the newer any_connection. - boost::mysql::any_connection conn(yield.get_executor()); - - // Connection configuration. By default, connections use the utf8mb4 character set - // (MySQL's name for regular UTF-8). - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(args.server_hostname); - params.username = args.username; - params.password = args.password; - params.database = "boost_mysql_examples"; - - // Connect to the server.with_diagnostics will turn any thrown exceptions - // into error_with_diagnostics, which contain more info than regular exceptions - conn.async_connect(params, with_diagnostics(yield)); - - // Compose the query. format_opts() returns a system::result, - // containing the options required by format_context. format_opts() may return - // an error if the connection doesn't know which character set is using - - // use async_set_character_set if this happens. - std::string query = compose_get_employees_query( - conn.format_opts().value(), - args.filts, - args.order_by - ); - - // Execute the query as usual. Note that, unlike with prepared statements, - // formatting happened in the client, and not in the server. - // Casting to string_view saves a copy in async_execute - boost::mysql::results result; - conn.async_execute(string_view(query), result, with_diagnostics(yield)); - - // Print the employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } + asio::io_context ctx; - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.async_close(with_diagnostics(yield)); - }, + // Launch our coroutine + asio::co_spawn( + ctx, + [&] { return coro_main(args); }, // If any exception is thrown in the coroutine body, rethrow it. [](std::exception_ptr ptr) { if (ptr) @@ -305,7 +309,7 @@ void main_impl(int argc, char** argv) } ); - // Don't forget to call run()! Otherwise, your program will do nothing. + // Calling run will actually execute the coroutine until completion ctx.run(); } @@ -334,3 +338,15 @@ int main(int argc, char** argv) } //] + +#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/metadata.cpp b/example/2_simple/metadata.cpp new file mode 100644 index 000000000..f69154164 --- /dev/null +++ b/example/2_simple/metadata.cpp @@ -0,0 +1,182 @@ +// +// 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_metadata + +/** + * This example shows how to obtain metadata from SQL queries, + * including field and table names. + * + * This example 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 + +namespace asio = boost::asio; +namespace mysql = boost::mysql; + +// The main coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password +) +{ + // Create a connection. + // Will use the same executor as the coroutine. + mysql::any_connection conn(co_await asio::this_coro::executor); + + // By default, string metadata (like column names) won't be retained. + // This is for efficiency reasons. You can change this setting by calling + // connection::set_meta_mode. It will affect any subsequent queries and statement executions. + conn.set_meta_mode(mysql::metadata_mode::full); + + // The socket path, 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.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + // Issue the query + constexpr const char* sql = R"( + SELECT comp.name AS company_name, emp.id AS employee_id + FROM employee emp + JOIN company comp ON (comp.id = emp.company_id) + )"; + mysql::results result; + co_await conn.async_execute(sql, result); + + /** + * results objects allow you to access metadata about the columns in the query + * using the meta() function, which returns span-like object containing metadata objects + * (one per column in the query, and in the same order as in the query). + * You can retrieve the column name, type, number of decimals, + * suggested display width, whether the column is part of a key... + * These metadata objects are owned by the results object. + */ + assert(result.meta().size() == 2); + + // <- + // clang-format off + // -> + const mysql::metadata& company_name = result.meta()[0]; + assert(company_name.database() == "boost_mysql_examples"); // database name + assert(company_name.table() == "comp"); // the alias we assigned to the table in the query + assert(company_name.original_table() == "company"); // the original table name + assert(company_name.column_name() == "company_name"); // the name of the column in the query + assert(company_name.original_column_name() == "name"); // the name of the physical column in the table + assert(company_name.type() == boost::mysql::column_type::varchar); // we created the column as a VARCHAR + assert(!company_name.is_primary_key()); // column is not a primary key + assert(!company_name.is_auto_increment()); // column is not AUTO_INCREMENT + assert(company_name.is_not_null()); // column may not be NULL + + const mysql::metadata& employee_id = result.meta()[1]; + assert(employee_id.database() == "boost_mysql_examples"); // database name + assert(employee_id.table() == "emp"); // the alias we assigned to the table in the query + assert(employee_id.original_table() == "employee"); // the original table name + assert(employee_id.column_name() == "employee_id"); // the name of the column in the query + assert(employee_id.original_column_name() == "id"); // the name of the physical column in the table + assert(employee_id.type() == boost::mysql::column_type::int_); // we created the column as INT + assert(employee_id.is_primary_key()); // column is a primary key + assert(employee_id.is_auto_increment()); // we declared the column as AUTO_INCREMENT + assert(employee_id.is_not_null()); // column cannot be NULL + // <- + // clang-format on + // avoid warnings in release mode + static_cast(company_name); + static_cast(employee_id); + // -> + + // 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 != 4) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2]); }, + // 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/2_simple/multi_function.cpp b/example/2_simple/multi_function.cpp new file mode 100644 index 000000000..fd8845029 --- /dev/null +++ b/example/2_simple/multi_function.cpp @@ -0,0 +1,152 @@ +// +// 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_multi_function + +/** + * This example demonstrates how to run multi-function operations + * to dump an entire table to stdout, reading rows in batches. + * + * 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. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +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) +} + +// The main coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password +) +{ + // Create a connection. It will use the same executor as our coroutine + mysql::any_connection conn(co_await asio::this_coro::executor); + + // The hostname, 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.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + // Start our query as a multi-function operation. + // This will send the query for execution but won't read the rows. + // An execution_state keep tracks of the operation. + mysql::execution_state st; + co_await conn.async_start_execution("SELECT first_name, last_name, salary FROM employee", st); + + // st.should_read_rows() returns true while there are more rows to read. + // Use async_read_some_rows to read a batch of rows. + // This function tries to minimize copies. employees is a view + // object pointing into the connection's internal buffers, + // and is valid until you start the next async operation. + while (st.should_read_rows()) + { + mysql::rows_view employees = co_await conn.async_read_some_rows(st); + for (auto employee : employees) + print_employee(employee); + } + + // 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 != 4) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2]); }, + // 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 \ No newline at end of file diff --git a/example/2_simple/multi_queries_transactions.cpp b/example/2_simple/multi_queries_transactions.cpp new file mode 100644 index 000000000..839f79c0d --- /dev/null +++ b/example/2_simple/multi_queries_transactions.cpp @@ -0,0 +1,187 @@ +// +// 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_multi_queries_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. + * + * The program updates the first name of an employee, + * and prints the employee's 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. + * + * 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 + +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::int64_t employee_id, + std::string_view new_first_name +) +{ + // Create a connection. + // Will use the same executor as the coroutine. + mysql::any_connection conn(co_await asio::this_coro::executor); + + //[section_connection_establishment_multi_queries + // 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. + 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"; + params.multi_queries = true; + + // 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. + // 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; + 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};" + "COMMIT", + employee_id, + new_first_name + ), + 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()) + { + 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; + } + + // 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 != 6) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2], std::stoi(argv[4]), argv[5]); }, + // 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/patch_updates.cpp b/example/2_simple/patch_updates.cpp similarity index 64% rename from example/patch_updates.cpp rename to example/2_simple/patch_updates.cpp index d34ce270b..864fecf02 100644 --- a/example/patch_updates.cpp +++ b/example/2_simple/patch_updates.cpp @@ -5,30 +5,45 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT + //[example_patch_updates -// Uses client-side SQL formatting to implement dynamic updates -// with PATCH-like semantics. -// The program updates an employee by ID, modifying fields -// as provided by command-line arguments, and leaving all other -// fields unmodified. +/** + * This example demonstrates how to implement dynamic updates + * with PATCH-like semantics using client-side SQL formatting. + * + * The program updates an employee by ID, modifying fields + * as provided by command-line arguments, and leaving all other + * fields unmodified. + * + * This example uses C++20 coroutines. If you need, you can backport + * it to C++14 (required by Boost.Describe) 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 -using boost::mysql::field_view; -using boost::mysql::string_view; +namespace mysql = boost::mysql; +namespace asio = boost::asio; /** * Represents a single update as a name, value pair. @@ -39,24 +54,24 @@ using boost::mysql::string_view; struct update_field { // The field name to set (i.e. the column name) - string_view field_name; + std::string_view field_name; // The value to set the field to. Recall that field_view is // a variant-like type that can hold all types that MySQL supports. - field_view field_value; + mysql::field_view field_value; }; // Contains the parsed command-line arguments struct cmdline_args { // MySQL username to use during authentication. - string_view username; + std::string_view username; // MySQL password to use during authentication. - string_view password; + std::string_view password; // Hostname where the MySQL server is listening. - string_view server_hostname; + std::string_view server_hostname; // The ID of the employee we want to update. std::int64_t employee_id{}; @@ -69,10 +84,10 @@ struct cmdline_args static cmdline_args parse_cmdline_args(int argc, char** argv) { // Available options - const string_view company_id_prefix = "--company-id="; - const string_view first_name_prefix = "--first-name="; - const string_view last_name_prefix = "--last-name="; - const string_view salary_prefix = "--salary="; + constexpr std::string_view company_id_prefix = "--company-id="; + constexpr std::string_view first_name_prefix = "--first-name="; + constexpr std::string_view last_name_prefix = "--last-name="; + constexpr std::string_view salary_prefix = "--salary="; // Helper function to print the usage message and exit auto print_usage_and_exit = [argv]() { @@ -96,28 +111,28 @@ static cmdline_args parse_cmdline_args(int argc, char** argv) for (int i = 5; i < argc; ++i) { // Get the argument - string_view arg = argv[i]; + std::string_view arg = argv[i]; // Attempt to match it with the options we have if (arg.starts_with(company_id_prefix)) { - string_view new_value = arg.substr(company_id_prefix.size()); - res.updates.push_back(update_field{"company_id", field_view(new_value)}); + std::string_view new_value = arg.substr(company_id_prefix.size()); + res.updates.push_back(update_field{"company_id", mysql::field_view(new_value)}); } else if (arg.starts_with(first_name_prefix)) { - string_view new_value = arg.substr(first_name_prefix.size()); - res.updates.push_back(update_field{"first_name", field_view(new_value)}); + std::string_view new_value = arg.substr(first_name_prefix.size()); + res.updates.push_back(update_field{"first_name", mysql::field_view(new_value)}); } else if (arg.starts_with(last_name_prefix)) { - string_view new_value = arg.substr(last_name_prefix.size()); - res.updates.push_back(update_field{"last_name", field_view(new_value)}); + std::string_view new_value = arg.substr(last_name_prefix.size()); + res.updates.push_back(update_field{"last_name", mysql::field_view(new_value)}); } else if (arg.starts_with(salary_prefix)) { - double new_value = std::stod(arg.substr(salary_prefix.size())); - res.updates.push_back(update_field{"salary", field_view(new_value)}); + double new_value = std::stod(std::string(arg.substr(salary_prefix.size()))); + res.updates.push_back(update_field{"salary", mysql::field_view(new_value)}); } else { @@ -136,37 +151,30 @@ static cmdline_args parse_cmdline_args(int argc, char** argv) return res; } -void main_impl(int argc, char** argv) +// The main coroutine +asio::awaitable coro_main(const cmdline_args& args) { - // Parse the command line - cmdline_args args = parse_cmdline_args(argc, argv); - - // Create an I/O context, required by all I/O objects - boost::asio::io_context ctx; - - // Create a connection. Note that client-side SQL formatting - // requires us to use the newer any_connection. - boost::mysql::any_connection conn(ctx); + // Create a connection. + // Will use the same executor as the coroutine. + mysql::any_connection conn(co_await asio::this_coro::executor); - // Connection configuration. By default, connections use the utf8mb4 character set - // (MySQL's name for regular UTF-8). - // We will use multi-queries to make transaction handling simpler and more efficient. - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(args.server_hostname); + // The hostname, username, password and database to use + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(args.server_hostname)); params.username = args.username; - params.password = args.password; + params.password = std::string(args.password); params.database = "boost_mysql_examples"; params.multi_queries = true; // Connect to the server - conn.connect(params); + co_await conn.async_connect(params); // Formats an individual update. Used by sequence(). // For update_field{"first_name", "John"}, it generates the string // "`first_name` = 'John'" // Format contexts can build a query string incrementally, and are used by sequence() internally - auto update_format_fn = [](update_field upd, boost::mysql::format_context_base& ctx) { - boost::mysql::format_sql_to(ctx, "{:i} = {}", upd.field_name, upd.field_value); + auto update_format_fn = [](update_field upd, mysql::format_context_base& ctx) { + mysql::format_sql_to(ctx, "{:i} = {}", upd.field_name, upd.field_value); }; // Compose and execute the query. with_params will expand placeholders @@ -180,14 +188,14 @@ void main_impl(int argc, char** argv) // As in std::format, we can use explicit indices like {0} and {1} to reference arguments. // By default, sequence copies its input range, but we don't need this here, // so we disable the copy by calling ref() - boost::mysql::results result; - conn.execute( - boost::mysql::with_params( + mysql::results result; + co_await conn.async_execute( + mysql::with_params( "START TRANSACTION; " "UPDATE employee SET {0} WHERE id = {1}; " "SELECT first_name, last_name, salary, company_id FROM employee WHERE id = {1}; " "COMMIT", - boost::mysql::sequence(std::ref(args.updates), update_format_fn), + mysql::sequence(std::ref(args.updates), update_format_fn), args.employee_id ), result @@ -211,7 +219,32 @@ void main_impl(int argc, char** argv) << "\n salary: " << employee.at(2) << "\n company_id: " << employee.at(3) << std::endl; // Notify the MySQL server we want to quit, then close the underlying connection. - conn.close(); + co_await conn.async_close(); +} + +void main_impl(int argc, char** argv) +{ + // Parse the command line + cmdline_args args = parse_cmdline_args(argc, argv); + + // 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(args); }, + // 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) @@ -238,3 +271,15 @@ int main(int argc, char** argv) } //] + +#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/pipeline.cpp b/example/2_simple/pipeline.cpp new file mode 100644 index 000000000..225fc764d --- /dev/null +++ b/example/2_simple/pipeline.cpp @@ -0,0 +1,205 @@ +// +// 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_pipeline + +/** + * (EXPERIMENTAL) + * This example demonstrates how to use the pipeline API to prepare, + * execute and close statements in batch. + * Pipelines are a experimental API. + * + * 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. + * + * 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 + +namespace asio = boost::asio; +namespace mysql = boost::mysql; + +// Prepare several statements in batch. +// This is faster than preparing them one by one, as it saves round-trips to the server. +asio::awaitable> batch_prepare( + mysql::any_connection& conn, + std::span statements +) +{ + // Construct a pipeline request describing the work to be performed. + // There must be one prepare_statement_stage per statement to prepare + mysql::pipeline_request req; + for (auto stmt_sql : statements) + req.add_prepare_statement(stmt_sql); + + // Run the pipeline. + // stage_response is a variant-like type that can hold the response of any stage type. + std::vector pipe_res; + co_await conn.async_run_pipeline(req, pipe_res); + + // If we got here, all statements were prepared successfully. + // pipe_res contains as many elements as statements.size(), holding statement objects + // Extract them into a vector + std::vector res; + res.reserve(statements.size()); + for (const auto& stage_res : pipe_res) + res.push_back(stage_res.get_statement()); + co_return res; +} + +// 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 +) +{ + // 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 + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; + params.database = "boost_mysql_examples"; + + // Connect to server + co_await conn.async_connect(params); + + // Prepare the statements using the batch prepare function that we previously defined + const std::array stmt_sql{ + "INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)", + "INSERT INTO audit_log (msg) VALUES (?)" + }; + std::vector stmts = co_await batch_prepare(conn, stmt_sql); + + // Create a pipeline request to execute them. + // Warning: do NOT include the COMMIT statement in this pipeline. + // COMMIT must only be executed if all the previous statements succeeded. + // In a pipeline, all stages get executed, regardless of the outcome of previous stages. + // We say that COMMIT has a dependency on the result of previous stages. + mysql::pipeline_request req; + req.add_execute("START TRANSACTION") + .add_execute(stmts.at(0), company_id, "Juan", "Lopez") + .add_execute(stmts.at(0), company_id, "Pepito", "Rodriguez") + .add_execute(stmts.at(0), company_id, "Someone", "Random") + .add_execute(stmts.at(1), "Inserted 3 new emplyees"); + std::vector res; + + // Execute the pipeline + co_await conn.async_run_pipeline(req, res); + + // If we got here, all stages executed successfully. + // Since they were execution stages, the response contains a results object. + // Get the IDs of the newly created employees + auto id1 = res.at(1).as_results().last_insert_id(); + auto id2 = res.at(2).as_results().last_insert_id(); + auto id3 = res.at(3).as_results().last_insert_id(); + + // We can now commit our transaction and close the statements. + // Clear the request and populate it again + req.clear(); + req.add_execute("COMMIT").add_close_statement(stmts.at(0)).add_close_statement(stmts.at(1)); + + // Run it + co_await conn.async_run_pipeline(req, res); + + // If we got here, our insertions got committed. + std::cout << "Inserted employees: " << id1 << ", " << id2 << ", " << id3 << std::endl; + + // Notify the MySQL server we want to quit, then close the underlying connection. + co_await conn.async_close(); +} + +void main_impl(int argc, char** argv) +{ + if (argc != 5) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2], 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 + ctx.run(); +} + +int main(int argc, char** argv) +{ + try + { + main_impl(argc, argv); + } + catch (const 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() << '\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/2_simple/prepared_statements.cpp b/example/2_simple/prepared_statements.cpp new file mode 100644 index 000000000..8c1cb4c0f --- /dev/null +++ b/example/2_simple/prepared_statements.cpp @@ -0,0 +1,165 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT + +//[example_prepared_statements + +/** + * This example demonstrates how to prepare, execute + * and deallocate prepared statements. This program retrieves + * all employees in a company, given its ID. + * + * 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. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +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) +} + +// 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 +) +{ + // Create a connection. It will use the same executor as our coroutine + mysql::any_connection conn(co_await asio::this_coro::executor); + + // The hostname, 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.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + + // Prepared statements can be used to execute queries with untrusted + // parameters securely. They are an option to mysql::with_params, + // but work server-side. + // They are more complex but can yield more efficiency when retrieving + // lots of numeric data, or when executing the same query several times with the same parameters. + // Ask the server to prepare a statement and retrieve its handle + mysql::statement stmt = co_await conn.async_prepare_statement( + "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?" + ); + + // Execute the statement. bind() must be passed as many parameters (number of ?) + // as the statement has. bind() packages the statement handle with the parameters, + // and async_execute sends them to the server + mysql::results result; + co_await conn.async_execute(stmt.bind(company_id), result); + for (mysql::row_view employee : result.rows()) + print_employee(employee); + + // We can execute stmt as many times as we want, potentially with different + // parameters, without the need to re-prepare it. + + // Once we're done with a statement, we can close it, to deallocate it from the server. + // Closing the connection will also deallocate active statements, so this is not + // strictly required here, but it's shown for completeness. + // This can be relevant if you're using long-lived sessions. + // Note that statement's destructor does NOT close the statement. + co_await conn.async_close_statement(stmt); + + // 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 != 5) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2], 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 + 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 \ No newline at end of file diff --git a/example/source_script.cpp b/example/2_simple/source_script.cpp similarity index 100% rename from example/source_script.cpp rename to example/2_simple/source_script.cpp diff --git a/example/2_simple/timeouts.cpp b/example/2_simple/timeouts.cpp new file mode 100644 index 000000000..6078e1088 --- /dev/null +++ b/example/2_simple/timeouts.cpp @@ -0,0 +1,167 @@ +// +// 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_timeouts + +/** + * 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 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. + */ + +#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) +} + +// 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 +) +{ + // 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 + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(server_hostname)); + params.username = username; + params.password = password; + params.database = "boost_mysql_examples"; + + // Connect to 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. + 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)) + ); + + // Print all the obtained rows + for (boost::mysql::row_view employee : result.rows()) + { + print_employee(employee); + } + + // 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 != 5) + { + 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 + 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); + } + } + ) + ); + + // 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() << '\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 \ No newline at end of file diff --git a/example/2_simple/tls_certificate_verification.cpp b/example/2_simple/tls_certificate_verification.cpp new file mode 100644 index 000000000..52225c4bd --- /dev/null +++ b/example/2_simple/tls_certificate_verification.cpp @@ -0,0 +1,189 @@ +// +// 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_tls_certificate_verification + +/** + * This example demonstrates how to set up TLS certificate verification + * and, more generally, how to pass custom TLS options to any_connection. + * + * 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. + * Additionally, your server must be configured with a trusted certificate + * with a common name of "mysql". + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +// The CA file that signed the server's certificate +constexpr const char CA_PEM[] = R"%(-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIUWznm2UoxXw3j7HCcp9PpiayTvFQwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAMBgNVBAoM +BW15c3FsMQ4wDAYDVQQDDAVteXNxbDAgFw0yMDA0MDQxNDMwMjNaGA8zMDE5MDgw +NjE0MzAyM1owQjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAM +BgNVBAoMBW15c3FsMQ4wDAYDVQQDDAVteXNxbDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN0WYdvsDb+a0TxOGPejcwZT0zvTrf921mmDUlrLN1Z0hJ/S +ydgQCSD7Q+6za4lTFZCXcvs52xvvS2gfC0yXyYLCT/jA4RQRxuF+/+w1gDWEbGk0 +KzEpsBuKrEIvEaVdoS78SxInnW/aegshdrRRocp4JQ6KHsZgkLTxSwPfYSUmMUo0 +cRO0Q/ak3VK8NP13A6ZFvZjrBxjS3cSw9HqilgADcyj1D4EokvfI1C9LrgwgLlZC +XVkjjBqqoMXGGlnXOEK+pm8bU68HM/QvMBkb1Amo8pioNaaYgqJUCP0Ch0iu1nUU +HtsWt6emXv0jANgIW0oga7xcT4MDGN/M+IRWLTECAwEAAaNTMFEwHQYDVR0OBBYE +FNxhaGwf5ePPhzK7yOAKD3VF6wm2MB8GA1UdIwQYMBaAFNxhaGwf5ePPhzK7yOAK +D3VF6wm2MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAoeJCAX +IDCFoAaZoQ1niI6Ac/cds8G8It0UCcFGSg+HrZ0YujJxWIruRCUG60Q2OAbEvn0+ +uRpTm+4tV1Wt92WFeuRyqkomozx0g4CyfsxGX/x8mLhKPFK/7K9iTXM4/t+xQC4f +J+iRmPVsMKQ8YsHYiWVhlOMH9XJQiqERCB2kOKJCH6xkaF2k0GbM2sGgbS7Z6lrd +fsFTOIVx0VxLVsZnWX3byE9ghnDR5jn18u30Cpb/R/ShxNUGIHqRa4DkM5la6uZX +W1fpSW11JBSUv4WnOO0C2rlIu7UJWOROqZZ0OsybPRGGwagcyff2qVRuI2XFvAMk +OzBrmpfHEhF6NDU= +-----END CERTIFICATE----- +)%"; + +// The main coroutine +asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password +) +{ + //[section_connection_establishment_tls_options + // Create a SSL context, which contains TLS configuration options + asio::ssl::context ssl_ctx(asio::ssl::context::tls_client); + + // Enable certificate verification. If the server's certificate + // is not valid or not signed by a trusted CA, async_connect will error. + ssl_ctx.set_verify_mode(asio::ssl::verify_peer); + + // Load a trusted CA, which was used to sign the server's certificate. + // This will allow the signature verification to succeed in our example. + // You will have to run your MySQL server with the test certificates + // located under $BOOST_MYSQL_ROOT/tools/ssl/ + // If you want to use your system's trusted CAs, use + // ssl::context::set_default_verify_paths() instead of this function. + ssl_ctx.add_certificate_authority(asio::buffer(CA_PEM)); + + // We expect the server certificate's common name to be "mysql". + // If it's not, the certificate will be rejected and handshake or connect will fail. + // Replace "mysql" by the common name you expect. + ssl_ctx.set_verify_callback(asio::ssl::host_name_verification("mysql")); + + // Create a connection. + // We pass the context as the second argument to the connection's constructor. + // Other TLS options can be also configured using this approach. + // We need to keep ssl_ctx alive as long as we use the connection. + mysql::any_connection conn(co_await asio::this_coro::executor, mysql::any_connection_params{&ssl_ctx}); + + // The hostname, 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.database = "boost_mysql_examples"; + + // Connect to the server. If certificate verification fails, + // async_connect will fail. + co_await conn.async_connect(params); + //] + + // The connection can now be used normally + mysql::results result; + co_await conn.async_execute("SELECT 'Hello world!'", result); + std::cout << result.rows().at(0).at(0) << 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 != 4) + { + 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 + asio::co_spawn( + ctx, + [=] { return coro_main(argv[3], argv[1], argv[2]); }, + // 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 \ No newline at end of file diff --git a/example/2_simple/unix_socket.cpp b/example/2_simple/unix_socket.cpp new file mode 100644 index 000000000..26f502e6f --- /dev/null +++ b/example/2_simple/unix_socket.cpp @@ -0,0 +1,141 @@ +// +// 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) && defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + +//[example_unix_socket + +/** + * This example demonstrates how to connect to MySQL using a UNIX socket. + * + * 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. + */ + +#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 unix_socket_path, + std::string_view username, + std::string_view password +) +{ + //[section_connection_establishment_unix_socket + // Create a connection. + // Will use the same executor as the coroutine. + mysql::any_connection conn(co_await asio::this_coro::executor); + + // The socket path, username, password and database to use. + // server_address is a variant-like type. Using emplace_unix_path, + // we can specify a UNIX socket path, instead of a hostname and a port. + // UNIX socket connections never use TLS. + mysql::connect_params params; + params.server_address.emplace_unix_path(std::string(unix_socket_path)); + params.username = username; + params.password = password; + params.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); + //] + + // The connection can now be used normally + mysql::results result; + co_await conn.async_execute("SELECT 'Hello world!'", result); + std::cout << result.rows().at(0).at(0) << 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 != 3 && argc != 4) + { + std::cerr << "Usage: " << argv[0] << " []\n"; + exit(1); + } + + // If not provided, use the default UNIX socket path, + // compatible with most UNIX systems. + const char* socket_path = argc >= 4 ? argv[3] : "/var/run/mysqld/mysqld.sock"; + + // 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(socket_path, argv[1], argv[2]); }, + // 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/system doesn't have the required capabilities to run this example" + << std::endl; +} + +#endif diff --git a/example/connection_pool/handle_request.cpp b/example/3_advanced/connection_pool/handle_request.cpp similarity index 100% rename from example/connection_pool/handle_request.cpp rename to example/3_advanced/connection_pool/handle_request.cpp diff --git a/example/connection_pool/handle_request.hpp b/example/3_advanced/connection_pool/handle_request.hpp similarity index 85% rename from example/connection_pool/handle_request.hpp rename to example/3_advanced/connection_pool/handle_request.hpp index dcabd3a0e..ac4164b02 100644 --- a/example/connection_pool/handle_request.hpp +++ b/example/3_advanced/connection_pool/handle_request.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_HANDLE_REQUEST_HPP -#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_HANDLE_REQUEST_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_HANDLE_REQUEST_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_HANDLE_REQUEST_HPP //[example_connection_pool_handle_request_hpp // diff --git a/example/connection_pool/log_error.hpp b/example/3_advanced/connection_pool/log_error.hpp similarity index 90% rename from example/connection_pool/log_error.hpp rename to example/3_advanced/connection_pool/log_error.hpp index 7f346a80e..12b7f7d56 100644 --- a/example/connection_pool/log_error.hpp +++ b/example/3_advanced/connection_pool/log_error.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_LOG_ERROR_HPP -#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_LOG_ERROR_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_LOG_ERROR_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_LOG_ERROR_HPP //[example_connection_pool_log_error_hpp // diff --git a/example/connection_pool/main.cpp b/example/3_advanced/connection_pool/main.cpp similarity index 79% rename from example/connection_pool/main.cpp rename to example/3_advanced/connection_pool/main.cpp index 01df7d817..77923de59 100644 --- a/example/connection_pool/main.cpp +++ b/example/3_advanced/connection_pool/main.cpp @@ -12,9 +12,26 @@ #ifdef BOOST_MYSQL_CXX14 //[example_connection_pool_main_cpp -// -// File: main.cpp -// + +/** + * This example demonstrates how to use a connection_pool. + * It implements a minimal REST API to manage notes. + * A note is a simple object containing a user-defined title and content. + * The REST API offers CRUD operations on such objects: + * POST /notes Creates a new note. + * GET /notes Retrieves all notes. + * GET /notes/ Retrieves a single note. + * PUT /notes/ Replaces a note, changing its title and content. + * DELETE /notes/ Deletes a note. + * + * Notes are stored in MySQL. The note_repository class encapsulates + * access to MySQL, offering friendly functions to manipulate notes. + * server.cpp encapsulates all the boilerplate to launch an HTTP server, + * match URLs to API endpoints, and invoke the relevant note_repository functions. + * All communication happens asynchronously. We use stackful coroutines to simplify + * development, using boost::asio::spawn and boost::asio::yield_context. + * This example requires linking to Boost::context, Boost::json and Boost::url. + */ #include #include @@ -33,23 +50,6 @@ #include "server.hpp" -// This example demonstrates how to use a connection_pool. -// It implements a minimal REST API to manage notes. -// A note is a simple object containing a user-defined title and content. -// The REST API offers CRUD operations on such objects: -// POST /notes Creates a new note. -// GET /notes Retrieves all notes. -// GET /notes/ Retrieves a single note. -// PUT /notes/ Replaces a note, changing its title and content. -// DELETE /notes/ Deletes a note. -// -// Notes are stored in MySQL. The note_repository class encapsulates -// access to MySQL, offering friendly functions to manipulate notes. -// server.cpp encapsulates all the boilerplate to launch an HTTP server, -// match URLs to API endpoints, and invoke the relevant note_repository functions. -// All communication happens asynchronously. We use stackful coroutines to simplify -// development, using boost::asio::spawn and boost::asio::yield_context. - using namespace notes; // The number of threads to use diff --git a/example/connection_pool/repository.cpp b/example/3_advanced/connection_pool/repository.cpp similarity index 100% rename from example/connection_pool/repository.cpp rename to example/3_advanced/connection_pool/repository.cpp diff --git a/example/connection_pool/repository.hpp b/example/3_advanced/connection_pool/repository.hpp similarity index 94% rename from example/connection_pool/repository.hpp rename to example/3_advanced/connection_pool/repository.hpp index 54ed3b852..f7aa90d2f 100644 --- a/example/connection_pool/repository.hpp +++ b/example/3_advanced/connection_pool/repository.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_REPOSITORY_HPP -#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_REPOSITORY_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_REPOSITORY_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_REPOSITORY_HPP //[example_connection_pool_repository_hpp // diff --git a/example/connection_pool/server.cpp b/example/3_advanced/connection_pool/server.cpp similarity index 100% rename from example/connection_pool/server.cpp rename to example/3_advanced/connection_pool/server.cpp diff --git a/example/connection_pool/server.hpp b/example/3_advanced/connection_pool/server.hpp similarity index 91% rename from example/connection_pool/server.hpp rename to example/3_advanced/connection_pool/server.hpp index f62283ebe..d1eb77f88 100644 --- a/example/connection_pool/server.hpp +++ b/example/3_advanced/connection_pool/server.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_SERVER_HPP -#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_SERVER_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_SERVER_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_SERVER_HPP //[example_connection_pool_server_hpp // diff --git a/example/connection_pool/types.hpp b/example/3_advanced/connection_pool/types.hpp similarity index 94% rename from example/connection_pool/types.hpp rename to example/3_advanced/connection_pool/types.hpp index be8a5a6bb..786ba1faf 100644 --- a/example/connection_pool/types.hpp +++ b/example/3_advanced/connection_pool/types.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_TYPES_HPP -#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_TYPES_HPP +#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_TYPES_HPP +#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_CONNECTION_POOL_TYPES_HPP //[example_connection_pool_types_hpp // diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index fd10c1f10..e91fe083b 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -22,142 +22,89 @@ target_link_libraries( boost_mysql_compiled ) -# Declare an example target -function(add_example_target EXAMPLE_NAME EXAMPLE_PATH) - add_executable(${EXAMPLE_NAME} ${EXAMPLE_PATH}) - target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_mysql_examples_common ${ARGN}) - boost_mysql_common_target_settings(${EXAMPLE_NAME}) -endfunction() -# Run an example through a Python runner -function(run_python_example EXAMPLE_TARGET RUNNER_NAME) - add_test( - NAME ${EXAMPLE_TARGET} - COMMAND - python - ${CMAKE_CURRENT_SOURCE_DIR}/private/${RUNNER_NAME} - $ - ${SERVER_HOST} - ) +function(add_example EXAMPLE_NAME) + # Parse the arguments + set(ONE_VALUE_ARGS PYTHON_RUNNER) + set(MULTI_VALUE_ARGS SOURCES LIBS ARGS) + cmake_parse_arguments(ADD_EXAMPLE "" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + + # Create the target + set(TARGET_NAME "boost_mysql_example_${EXAMPLE_NAME}") + add_executable(${TARGET_NAME} ${ADD_EXAMPLE_SOURCES}) + target_link_libraries(${TARGET_NAME} PRIVATE boost_mysql_examples_common) + boost_mysql_common_target_settings(${TARGET_NAME}) + target_link_libraries(${TARGET_NAME} PRIVATE ${ADD_EXAMPLE_LIBS}) + + # Add it as a test + if (ADD_EXAMPLE_PYTHON_RUNNER) + add_test( + NAME ${TARGET_NAME} + COMMAND + python + ${CMAKE_CURRENT_SOURCE_DIR}/private/${ADD_EXAMPLE_PYTHON_RUNNER} + $ + ${ADD_EXAMPLE_ARGS} + ) + else() + add_test( + NAME ${TARGET_NAME} + COMMAND ${TARGET_NAME} ${ADD_EXAMPLE_ARGS} + ) + endif() endfunction() -# Build and run an example -function(add_example EXAMPLE_NAME EXAMPLE_PATH) - add_example_target(${EXAMPLE_NAME} ${EXAMPLE_PATH}) - - add_test( - NAME ${EXAMPLE_NAME} - COMMAND ${EXAMPLE_NAME} ${ARGN} - ) +function(add_tutorial EXAMPLE_NAME EXAMPLE_PATH) + add_example(${EXAMPLE_NAME} SOURCES "1_tutorial/${EXAMPLE_PATH}" ${ARGN}) endfunction() -# Coroutines needs Boost.Context and shouldn't be memchecked -function(add_example_coroutines EXAMPLE_NAME) - set(EXECUTABLE_NAME boost_mysql_example_${EXAMPLE_NAME}) - add_example_target(${EXECUTABLE_NAME} ${EXAMPLE_NAME}.cpp Boost::context) - add_test( - NAME ${EXECUTABLE_NAME} - COMMAND ${EXECUTABLE_NAME} example_user example_password ${SERVER_HOST} - ) +function(add_simple_example EXAMPLE_NAME) + add_example(${EXAMPLE_NAME} SOURCES "2_simple/${EXAMPLE_NAME}.cpp" ${ARGN}) endfunction() -# The order management examples must be run several times through a Python script -function(add_example_order_management EXAMPLE_NAME EXAMPLE_PATH) - add_example_target(${EXAMPLE_NAME} ${EXAMPLE_PATH}) - run_python_example(${EXAMPLE_NAME} run_stored_procedures.py) -endfunction() - -# Regular examples are the ones that require no extra linking libs and can be run -# with example_user example_password -set(REGULAR_EXAMPLES - tutorial - async_callbacks - async_coroutinescpp20 - async_futures - metadata - ssl - timeouts - pipeline -) - -foreach(FILE_NAME ${REGULAR_EXAMPLES}) - add_example( - "boost_mysql_example_${FILE_NAME}" - "${FILE_NAME}.cpp" - example_user example_password ${SERVER_HOST} - ) -endforeach() - -# Order management examples must be run several times through a Python script -set(ORDER_EXAMPLES - prepared_statements_cpp11 - prepared_statements_cpp14 - stored_procedures_cpp11 - stored_procedures_cpp14 -) - -foreach(FILE_NAME ${ORDER_EXAMPLES}) - add_example_order_management( - "boost_mysql_example_${FILE_NAME}" - "order_management/${FILE_NAME}.cpp" - ) -endforeach() - -# Coroutine examples -add_example_coroutines(async_coroutines) -add_example_coroutines(any_connection) +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) + +# Simple +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) +add_simple_example(batch_inserts_generic ARGS ${SERVER_HOST} PYTHON_RUNNER run_batch_inserts.py LIBS Boost::json) +add_simple_example(patch_updates ARGS ${SERVER_HOST} PYTHON_RUNNER run_patch_updates.py) +add_simple_example(dynamic_filters ARGS ${SERVER_HOST} PYTHON_RUNNER run_dynamic_filters.py) +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 if (NOT WIN32) - add_example( - boost_mysql_example_unix_socket - unix_socket.cpp - example_user example_password - ) + add_simple_example(unix_socket ARGS example_user example_password) endif() -# Source script +# Advanced add_example( - boost_mysql_example_source_script - source_script.cpp - example_user example_password ${SERVER_HOST} ${CMAKE_CURRENT_SOURCE_DIR}/private/test_script.sql -) - -# Patch updates -add_example_target(boost_mysql_example_patch_updates patch_updates.cpp) -run_python_example(boost_mysql_example_patch_updates run_patch_updates.py) - -# Dynamic filters -add_example_target(boost_mysql_example_dynamic_filters dynamic_filters.cpp) -target_link_libraries(boost_mysql_example_dynamic_filters PRIVATE Boost::context) -run_python_example(boost_mysql_example_dynamic_filters run_dynamic_filters.py) - -# Batch inserts -add_example_target(boost_mysql_example_batch_inserts batch_inserts.cpp) -target_link_libraries(boost_mysql_example_batch_inserts PRIVATE Boost::json) -run_python_example(boost_mysql_example_batch_inserts run_batch_inserts.py) - -# Batch inserts, generic version -add_example_target(boost_mysql_example_batch_inserts_generic batch_inserts_generic.cpp) -target_link_libraries(boost_mysql_example_batch_inserts_generic PRIVATE Boost::json) -run_python_example(boost_mysql_example_batch_inserts_generic run_batch_inserts.py) - -# HTTP server -add_executable( - boost_mysql_example_connection_pool - connection_pool/repository.cpp - connection_pool/handle_request.cpp - connection_pool/server.cpp - connection_pool/main.cpp -) -target_link_libraries( - boost_mysql_example_connection_pool - PRIVATE - boost_mysql_examples_common - Boost::context - Boost::json - Boost::url - Boost::beast + connection_pool + SOURCES + 3_advanced/connection_pool/repository.cpp + 3_advanced/connection_pool/handle_request.cpp + 3_advanced/connection_pool/server.cpp + 3_advanced/connection_pool/main.cpp + LIBS + Boost::context + Boost::json + Boost::url + Boost::beast + PYTHON_RUNNER run_connection_pool.py + ARGS ${SERVER_HOST} ) -boost_mysql_common_target_settings(boost_mysql_example_connection_pool) -run_python_example(boost_mysql_example_connection_pool run_connection_pool.py) diff --git a/example/Jamfile b/example/Jamfile index 47c618fab..5aed45a68 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -6,6 +6,7 @@ # import os ; +import sequence ; path-constant this_dir : . ; @@ -19,17 +20,18 @@ if $(hostname) = "" # Builds and run a "regular" example rule run_regular_example ( example_name : - sources * + sources * : + args * ) { + local arg_str = [ sequence.join $(args) : " " ] ; run - "$(example_name).cpp" /boost/mysql/test//boost_mysql_compiled /boost/mysql/test//launch_with_valgrind $(sources) : requirements - "example_user example_password $(hostname)" - : target-name "boost_mysql_example_$(example_name)" + $(arg_str) + : target-name $(example_name) ; } @@ -46,66 +48,52 @@ rule run_python_example ( : requirements "python $(this_dir)/private/$(python_runner)" $(hostname) - : target-name "boost_mysql_example_$(example_name)" + : target-name $(example_name) ; } -# "Regular" examples -run_regular_example tutorial ; -run_regular_example async_callbacks ; -run_regular_example async_coroutinescpp20 ; -run_regular_example async_futures ; -run_regular_example metadata ; -run_regular_example ssl ; -run_regular_example timeouts ; -run_regular_example pipeline ; -run_regular_example async_coroutines : /boost/mysql/test//boost_context_lib ; -run_regular_example any_connection : /boost/mysql/test//boost_context_lib ; - -# Order management examples -local order_examples = - prepared_statements_cpp11 - prepared_statements_cpp14 - stored_procedures_cpp11 - stored_procedures_cpp14 -; +local regular_args = example_user example_password $(hostname) ; -for local example in $(order_examples) -{ - run_python_example $(example) : run_stored_procedures.py : order_management/$(example).cpp ; -} +# 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 ; -# Other examples that need a Python script -run_python_example patch_updates : run_patch_updates.py : patch_updates.cpp ; -run_python_example dynamic_filters : run_dynamic_filters.py : dynamic_filters.cpp /boost/mysql/test//boost_context_lib ; -run_python_example batch_inserts : run_batch_inserts.py : batch_inserts.cpp /boost/mysql/test//boost_json_lib ; -run_python_example batch_inserts_generic : run_batch_inserts.py : batch_inserts_generic.cpp /boost/mysql/test//boost_json_lib ; +# 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 - unix_socket.cpp + 2_simple/unix_socket.cpp /boost/mysql/test//boost_mysql_compiled + /boost/mysql/test//launch_with_valgrind : requirements windows:no "example_user example_password" ; -# Source script -run - source_script.cpp - /boost/mysql/test//boost_mysql_compiled - /boost/mysql/test//launch_with_valgrind - : requirements - "example_user example_password $(hostname) $(this_dir)/private/test_script.sql" - ; - # Connection pool run - connection_pool/main.cpp - connection_pool/repository.cpp - connection_pool/handle_request.cpp - connection_pool/server.cpp + 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 diff --git a/example/any_connection.cpp b/example/any_connection.cpp deleted file mode 100644 index e86101fd2..000000000 --- a/example/any_connection.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_any_connection - -// any_connection is a connection type that is easier to use than regular -// connection. It is type-erased: it's not a template, and is able to connect -// to any server using TCP, UNIX sockets and SSL. It features a simplified -// connect and async_connect function family, which handle name resolution. -// Performance is equivalent to regular connection. -// -// This example demonstrates how to connect to a server using any_connection. -// It uses asynchronous functions and coroutines (with boost::asio::spawn). -// Recall that using these coroutines requires linking against Boost.Context. - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -using boost::mysql::with_diagnostics; - -void print_employee(boost::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) -} - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - const char* hostname = argv[3]; - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // I/O context - boost::asio::io_context ctx; - - // Connection. Note that the connection's type doesn't depend - // on the transport (TCP or UNIX sockets). - boost::mysql::any_connection conn(ctx); - - // Connection configuration. This contains the server address, - // credentials, and other configuration used during connection establishment. - // Note that, by default, TCP connections will use TLS. connect_params::ssl - // allows disabling it. - boost::mysql::connect_params params; - - // The server address. This can either be a host and port or a UNIX socket path - params.server_address.emplace_host_and_port(hostname); - - // Username to log in as - params.username = argv[1]; - - // Password to use - params.password = argv[2]; - - // Database to use; leave empty or omit for no database - params.database = "boost_mysql_examples"; - - /** - * The entry point. We spawn a stackful coroutine using boost::asio::spawn. - * - * The coroutine will actually start running when we call io_context::run(). - * It will suspend every time we call one of the asynchronous functions, saving - * all information it needs for resuming. When the asynchronous operation completes, - * the coroutine will resume in the point it was left. - */ - boost::asio::spawn( - ctx.get_executor(), - [&conn, ¶ms, company_id](boost::asio::yield_context yield) { - // Connect to the server. This will take care of resolving the provided - // hostname to an IP address, connect to that address, and establish - // the MySQL session. - // with_diagnostics will turn any thrown exceptions - // into error_with_diagnostics, which contain more info than regular exceptions - conn.async_connect(params, with_diagnostics(yield)); - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - boost::mysql::statement stmt = conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", - with_diagnostics(yield) - ); - - // Execute the statement - boost::mysql::results result; - conn.async_execute(stmt.bind(company_id), result, with_diagnostics(yield)); - - // Print the employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.async_close(with_diagnostics(yield)); - }, - // If any exception is thrown in the coroutine body, rethrow it. - [](std::exception_ptr ptr) { - if (ptr) - { - std::rethrow_exception(ptr); - } - } - ); - - // Don't forget to call run()! Otherwise, your program - // will not spawn the coroutine and will do nothing. - ctx.run(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // You will only get this type of exceptions if you use with_diagnostics. - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's character set - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/async_callbacks.cpp b/example/async_callbacks.cpp deleted file mode 100644 index 1fe3fce4a..000000000 --- a/example/async_callbacks.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_async_callbacks - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -using boost::mysql::error_code; - -void print_employee(boost::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) -} - -class application -{ - boost::asio::ip::tcp::resolver::results_type eps; // Physical endpoint(s) to connect to - boost::mysql::handshake_params conn_params; // MySQL credentials and other connection config - boost::asio::io_context ctx; // boost::asio context - boost::asio::ip::tcp::resolver resolver; // To perform hostname resolution - boost::asio::ssl::context ssl_ctx; // MySQL 8+ default settings require SSL - boost::mysql::tcp_ssl_connection conn; // Represents the connection to the MySQL server - boost::mysql::statement stmt; // A prepared statement - boost::mysql::results result; // A result from a query - boost::mysql::error_code errc; // Will be set in case of error - boost::mysql::diagnostics diag; // Will be populated with info about server errors - const char* company_id; // The ID of the company whose employees we want to list. Untrusted. -public: - application(const char* username, const char* password, const char* company_id) - : conn_params(username, password, "boost_mysql_examples"), - resolver(ctx.get_executor()), - ssl_ctx(boost::asio::ssl::context::tls_client), - conn(ctx, ssl_ctx), - company_id(company_id) - { - } - - error_code get_error() const { return errc; } - const boost::mysql::diagnostics& get_diagnostics() const { return diag; } - - void start(const char* hostname) { resolve_hostname(hostname); } - - void resolve_hostname(const char* hostname) - { - resolver.async_resolve( - hostname, - boost::mysql::default_port_string, - [this](error_code err, boost::asio::ip::tcp::resolver::results_type results) { - errc = err; - if (!err) - { - eps = std::move(results); - connect(); - } - } - ); - } - - void connect() - { - conn.async_connect(*eps.begin(), conn_params, diag, [this](error_code err) { - errc = err; - if (!err) - prepare_statement(); - }); - } - - void prepare_statement() - { - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", - diag, - [this](error_code err, boost::mysql::statement temp_stmt) { - errc = err; - if (!err) - { - stmt = temp_stmt; - query_employees(); - } - } - ); - } - - void query_employees() - { - conn.async_execute(stmt.bind(company_id), result, diag, [this](error_code err) { - errc = err; - if (!err) - { - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - close(); - } - }); - } - - void close() - { - // Notify the MySQL server we want to quit and then close the socket - conn.async_close(diag, [this](error_code err) { errc = err; }); - } - - void run() { ctx.run(); } -}; - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - application app(argv[1], argv[2], company_id); - app.start(argv[3]); // starts the async chain - app.run(); // run the asio::io_context until the async chain finishes - - // Check for errors - if (error_code ec = app.get_error()) - { - std::cerr << "Error: " << ec << ": " << ec.message() << '\n' - << "Server diagnostics: " << app.get_diagnostics().server_message() << std::endl; - exit(1); - } -} - -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; - } -} - -//] diff --git a/example/async_coroutines.cpp b/example/async_coroutines.cpp deleted file mode 100644 index 0077b56c3..000000000 --- a/example/async_coroutines.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_async_coroutines - -// To use coroutines created by boost::asio::spawn, you need to link -// against Boost.Context. - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -using boost::mysql::with_diagnostics; - -void print_employee(boost::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) -} - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - const char* hostname = argv[3]; - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Connection params - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit for no database - ); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - /** - * The entry point. We spawn a stackful coroutine using boost::asio::spawn. - * - * The coroutine will actually start running when we call io_context::run(). - * It will suspend every time we call one of the asynchronous functions, saving - * all information it needs for resuming. When the asynchronous operation completes, - * the coroutine will resume in the point it was left. - */ - boost::asio::spawn( - ctx.get_executor(), - [&conn, &resolver, params, hostname, company_id](boost::asio::yield_context yield) { - // Hostname resolution - auto endpoints = resolver.async_resolve(hostname, boost::mysql::default_port_string, yield); - - // Connect to server. with_diagnostics will turn any thrown exceptions - // into error_with_diagnostics, which contain more info than regular exceptions - conn.async_connect(*endpoints.begin(), params, with_diagnostics(yield)); - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - boost::mysql::statement stmt = conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", - with_diagnostics(yield) - ); - - // Execute the statement - boost::mysql::results result; - conn.async_execute(stmt.bind(company_id), result, with_diagnostics(yield)); - - // Print the employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.async_close(with_diagnostics(yield)); - }, - // If any exception is thrown in the coroutine body, rethrow it. - [](std::exception_ptr ptr) { - if (ptr) - { - std::rethrow_exception(ptr); - } - } - ); - - // Don't forget to call run()! Otherwise, your program - // will not spawn the coroutine and will do nothing. - ctx.run(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // You will only get this type of exceptions if you use with_diagnostics. - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's character set - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/async_coroutinescpp20.cpp b/example/async_coroutinescpp20.cpp deleted file mode 100644 index 00e60f0df..000000000 --- a/example/async_coroutinescpp20.cpp +++ /dev/null @@ -1,173 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_async_coroutinescpp20 - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifdef BOOST_ASIO_HAS_CO_AWAIT - -void print_employee(boost::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) -} - -/** - * Our coroutine. It must have a return type of boost::asio::awaitable. - * Our coroutine does not communicate any result back, so T=void. - * Remember that you do not have to explicitly create any awaitable in - * your function. Instead, the return type is fed to std::coroutine_traits - * to determine the semantics of the coroutine, like the promise type. - * Asio already takes care of all this for us. - * - * The coroutine will suspend every time we call one of the asynchronous functions, saving - * all information it needs for resuming. When the asynchronous operation completes, - * the coroutine will resume in the point it was left. - * - * The return type of an asynchronous operation that uses use_awaitable - * as completion token is a boost::asio::awaitable, where T - * is the second argument to the handler signature for the asynchronous operation. - * If any of the asynchronous operations fail, an exception will be raised - * within the coroutine. - * - * Note that we're not specifying any completion token to our initiating functions. - * The default token for Boost.MySQL is mysql::with_diagnostics(asio::deferred), - * which allows using co_await and throws on error. - */ -boost::asio::awaitable coro_main( - boost::mysql::tcp_ssl_connection& conn, - boost::asio::ip::tcp::resolver& resolver, - const boost::mysql::handshake_params& params, - const char* hostname, - const char* company_id -) -{ - // Resolve hostname. We may use use_awaitable here, as hostname resolution - // never produces any diagnostics. - auto endpoints = co_await resolver.async_resolve(hostname, boost::mysql::default_port_string); - - // Connect to server - co_await conn.async_connect(*endpoints.begin(), params); - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - boost::mysql::statement stmt = co_await conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?" - ); - - // Execute the statement - boost::mysql::results result; - co_await conn.async_execute(stmt.bind(company_id), result); - - // Print all employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // 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 != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - const char* hostname = argv[3]; - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Connection parameters - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit the parameter for no - // database - ); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // The entry point. We pass in a function returning - // boost::asio::awaitable, as required. - boost::asio::co_spawn( - ctx.get_executor(), - [&conn, &resolver, params, hostname, company_id] { - return coro_main(conn, resolver, params, hostname, company_id); - }, - // If any exception is thrown in the coroutine body, rethrow it. - [](std::exception_ptr ptr) { - if (ptr) - { - std::rethrow_exception(ptr); - } - } - ); - - // Calling run will execute the requested operations. - ctx.run(); -} - -#else - -void main_impl(int, char**) -{ - std::cout << "Sorry, your compiler does not support C++20 coroutines" << std::endl; -} - -#endif - -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() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/async_futures.cpp b/example/async_futures.cpp deleted file mode 100644 index 8c0199977..000000000 --- a/example/async_futures.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_async_futures - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include - -using boost::asio::use_future; -using boost::mysql::error_code; -using boost::mysql::with_diagnostics; - -void print_employee(boost::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) -} - -/** - * A boost::asio::io_context plus a thread that calls context.run(). - * We encapsulate this here to ensure correct shutdown even in case of - * error (exception), when we should first reset the work guard, then - * stop the io_context, and then join the thread. Failing to do so - * may cause your application to not stop (if the work guard is not - * reset) or to terminate badly (if the thread is not joined). - */ -class application -{ - boost::asio::io_context ctx_; - boost::asio::executor_work_guard guard_; - std::thread runner_; - -public: - application() : guard_(ctx_.get_executor()), runner_([this] { ctx_.run(); }) {} - application(const application&) = delete; - application(application&&) = delete; - application& operator=(const application&) = delete; - application& operator=(application&&) = delete; - ~application() - { - guard_.reset(); - runner_.join(); - } - boost::asio::io_context& context() { return ctx_; } -}; - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // Context and connections - application app; // boost::asio::io_context and a thread that calls run() - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - boost::mysql::tcp_ssl_connection conn(app.context(), ssl_ctx); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(app.context().get_executor()); - - // Connection params - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit for no database - ); - - /** - * Hostname resolution. - * Calling async_resolve triggers the - * operation, and calling future::get() blocks the current thread until - * it completes. get() will throw an exception if the operation fails. - */ - auto endpoints_fut = resolver.async_resolve( - argv[3], - boost::mysql::default_port_string, - boost::asio::use_future - ); - auto endpoints = endpoints_fut.get(); - - // Perform the TCP connect and MySQL handshake. - // with_diagnostics will turn any thrown exceptions - // into error_with_diagnostics, which contain more info than regular exceptions - std::future fut = conn.async_connect(*endpoints.begin(), params, with_diagnostics(use_future)); - fut.get(); - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - std::future stmt_fut = conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", - with_diagnostics(use_future) - ); - boost::mysql::statement stmt = stmt_fut.get(); - - // Execute the statement - boost::mysql::results result; - fut = conn.async_execute(stmt.bind(company_id), result, with_diagnostics(use_future)); - fut.get(); - - // Print employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.async_close(with_diagnostics(use_future)).get(); - - // application dtor. stops io_context and then joins the thread -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // You will only get this type of exceptions if you use with_diagnostics. - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's character set - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/db_setup.sql b/example/db_setup.sql index f360ac38d..fade08ccd 100644 --- a/example/db_setup.sql +++ b/example/db_setup.sql @@ -86,3 +86,300 @@ CREATE USER 'example_user'@'%' IDENTIFIED WITH 'mysql_native_password'; ALTER USER 'example_user'@'%' IDENTIFIED BY 'example_password'; GRANT ALL PRIVILEGES ON boost_mysql_examples.* TO 'example_user'@'%'; FLUSH PRIVILEGES; + + +-- Tables for the orders examples +CREATE TABLE products ( + id INT PRIMARY KEY AUTO_INCREMENT, + short_name VARCHAR(100) NOT NULL, + descr TEXT, + price INT NOT NULL, + FULLTEXT(short_name, descr) +); + +CREATE TABLE orders( + id INT PRIMARY KEY AUTO_INCREMENT, + `status` ENUM('draft', 'pending_payment', 'complete') NOT NULL DEFAULT 'draft' +); + +CREATE TABLE order_items( + id INT PRIMARY KEY AUTO_INCREMENT, + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); + +-- Procedures for the orders examples +DELIMITER // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE get_products(IN p_search VARCHAR(50)) +BEGIN + DECLARE max_products INT DEFAULT 20; + IF p_search IS NULL THEN + SELECT id, short_name, descr, price + FROM products + LIMIT max_products; + ELSE + SELECT id, short_name, descr, price FROM products + WHERE MATCH(short_name, descr) AGAINST(p_search) + LIMIT max_products; + END IF; +END // + +CREATE PROCEDURE create_order() +BEGIN + START TRANSACTION; + + -- Create the order + INSERT INTO orders () VALUES (); + + -- Return the order + SELECT id, `status` + FROM orders + WHERE id = LAST_INSERT_ID(); + + COMMIT; +END // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE get_order( + IN p_order_id INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION READ ONLY; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'get_order: invalid parameters'; + END IF; + + -- Check that the order exists + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + + -- Return the order. The IFNULL statements make MySQL correctly report the fields as non-NULL + SELECT + IFNULL(p_order_id, 0) AS id, + IFNULL(order_status, 'draft') AS `status`; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE get_orders() +BEGIN + SELECT id, `status` FROM orders; +END // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE add_line_item( + IN p_order_id INT, + IN p_product_id INT, + IN p_quantity INT, + OUT pout_line_item_id INT +) +BEGIN + DECLARE product_price INT; + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL OR p_product_id IS NULL OR p_quantity IS NULL OR p_quantity <= 0 THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'add_line_item: invalid params'; + END IF; + + -- Ensure that the product is valid + SELECT price INTO product_price FROM products WHERE id = p_product_id; + IF product_price IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given product does not exist'; + END IF; + + -- Get the order + SELECT `status` INTO order_status FROM orders WHERE id = p_order_id; + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not editable'; + END IF; + + -- Insert the new item + INSERT INTO order_items (order_id, product_id, quantity) VALUES (p_order_id, p_product_id, p_quantity); + + -- Return value + SET pout_line_item_id = LAST_INSERT_ID(); + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE remove_line_item( + IN p_line_item_id INT +) +BEGIN + DECLARE order_id INT; + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_line_item_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'remove_line_item: invalid params'; + END IF; + + -- Get the order + SELECT orders.id, orders.`status` + INTO order_id, order_status + FROM orders + JOIN order_items items ON (orders.id = items.order_id) + WHERE items.id = p_line_item_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order item does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not editable'; + END IF; + + -- Delete the line item + DELETE FROM order_items + WHERE id = p_line_item_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = order_id; + + COMMIT; +END // + +CREATE DEFINER = 'example_user'@'%' PROCEDURE checkout_order( + IN p_order_id INT, + OUT pout_order_total INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'checkout_order: invalid params'; + END IF; + + -- Get the order + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'draft' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be checked out'; + END IF; + + -- Update the order + UPDATE orders SET `status` = 'pending_payment' WHERE id = p_order_id; + + -- Retrieve the total price + SELECT SUM(prod.price * item.quantity) + INTO pout_order_total + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + + +CREATE DEFINER = 'example_user'@'%' PROCEDURE complete_order( + IN p_order_id INT +) +BEGIN + DECLARE order_status TEXT; + START TRANSACTION; + + -- Check parameters + IF p_order_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'complete_order: invalid params'; + END IF; + + -- Get the order + SELECT `status` + INTO order_status + FROM orders WHERE id = p_order_id; + + IF order_status IS NULL THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; + END IF; + IF order_status <> 'pending_payment' THEN + SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be completed'; + END IF; + + -- Update the order + UPDATE orders SET `status` = 'complete' WHERE id = p_order_id; + + -- Return the edited order + SELECT id, `status` + FROM orders WHERE id = p_order_id; + SELECT + item.id AS id, + item.quantity AS quantity, + prod.price AS unit_price + FROM order_items item + JOIN products prod ON item.product_id = prod.id + WHERE item.order_id = p_order_id; + + COMMIT; +END // + +DELIMITER ; + +-- Create an order, at least +INSERT INTO orders () VALUES (); + +-- Contents for the products table +INSERT INTO products (price, short_name, descr) VALUES + (6400, 'A Feast for Odin', 'A Feast for Odin is a points-driven game, with plethora of pathways to victory, with a range of risk balanced against reward. A significant portion of this is your central hall, which has a whopping -86 points of squares and a major part of your game is attempting to cover these up with various tiles. Likewise, long halls and island colonies can also offer large rewards, but they will have penalties of their own.'), + (1600, 'Railroad Ink', 'The critically acclaimed roll and write game where you draw routes on your board trying to connect the exits at its edges. The more you connect, the more points you make, but beware: each incomplete route will make you lose points!'), + (4000, 'Catan', 'Catan is a board game for two to four players in which you compete to gather resources and build the biggest settlements on the fictional island of Catan. It takes approximately one hour to play.'), + (2500, 'Not Alone', 'It is the 25th century. You are a member of an intergalactic expedition shipwrecked on a mysterious planet named Artemia. While waiting for the rescue ship, you begin to explore the planet but an alien entity picks up your scent and begins to hunt you. You are NOT ALONE! Will you survive the dangers of Artemia?'), + (4500, 'Dice Hospital', "In Dice Hospital, a worker placement board game, players are tasked with running a local hospital. Each round you'll be admitting new patients, hiring specialists, building new departments, and treating as many incoming patients as you can.") +; diff --git a/example/metadata.cpp b/example/metadata.cpp deleted file mode 100644 index 995215ca0..000000000 --- a/example/metadata.cpp +++ /dev/null @@ -1,130 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_metadata - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#define ASSERT(expr) \ - if (!(expr)) \ - { \ - std::cerr << "Assertion failed: " #expr << std::endl; \ - exit(1); \ - } - -void main_impl(int argc, char** argv) -{ - if (argc != 4) - { - std::cerr << "Usage: " << argv[0] << " \n"; - exit(1); - } - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // By default, string metadata (like column names) won't be retained. - // This is for efficiency reasons. You can change this setting by calling - // connection::set_meta_mode. It will affect any subsequent queries and statement executions. - conn.set_meta_mode(boost::mysql::metadata_mode::full); - - // Hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - auto endpoints = resolver.resolve(argv[3], boost::mysql::default_port_string); - - // TCP and MySQL level connect - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit the parameter for no - // database - ); - conn.connect(*endpoints.begin(), params); - - // Issue the query - const char* sql = R"( - SELECT comp.name AS company_name, emp.id AS employee_id - FROM employee emp - JOIN company comp ON (comp.id = emp.company_id) - )"; - boost::mysql::results result; - conn.execute(sql, result); - - /** - * results objects allow you to access metadata about the columns in the query - * using the meta() function, which returns span-like object containing metadata objects - * (one per column in the query, and in the same order as in the query). - * You can retrieve the column name, type, number of decimals, - * suggested display width, whether the column is part of a key... - * These metadata objects are owned by the results object. - */ - ASSERT(result.meta().size() == 2); - - // clang-format off - const boost::mysql::metadata& company_name = result.meta()[0]; - ASSERT(company_name.database() == "boost_mysql_examples"); // database name - ASSERT(company_name.table() == "comp"); // the alias we assigned to the table in the query - ASSERT(company_name.original_table() == "company"); // the original table name - ASSERT(company_name.column_name() == "company_name"); // the name of the column in the query - ASSERT(company_name.original_column_name() == "name"); // the name of the physical column in the table - ASSERT(company_name.type() == boost::mysql::column_type::varchar); // we created the column as a VARCHAR - ASSERT(!company_name.is_primary_key()); // column is not a primary key - ASSERT(!company_name.is_auto_increment()); // column is not AUTO_INCREMENT - ASSERT(company_name.is_not_null()); // column may not be NULL - - const boost::mysql::metadata& employee_id = result.meta()[1]; - ASSERT(employee_id.database() == "boost_mysql_examples"); // database name - ASSERT(employee_id.table() == "emp"); // the alias we assigned to the table in the query - ASSERT(employee_id.original_table() == "employee"); // the original table name - ASSERT(employee_id.column_name() == "employee_id"); // the name of the column in the query - ASSERT(employee_id.original_column_name() == "id"); // the name of the physical column in the table - ASSERT(employee_id.type() == boost::mysql::column_type::int_); // we created the column as INT - ASSERT(employee_id.is_primary_key()); // column is a primary key - ASSERT(employee_id.is_auto_increment()); // we declared the column as AUTO_INCREMENT - ASSERT(employee_id.is_not_null()); // column cannot be NULL - // clang-format on - - // Close the connection - conn.close(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's 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; - } -} - -//] diff --git a/example/order_management/db_setup.sql b/example/order_management/db_setup.sql deleted file mode 100644 index a39ff2b42..000000000 --- a/example/order_management/db_setup.sql +++ /dev/null @@ -1,314 +0,0 @@ --- --- Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) --- --- Distributed under the Boost Software License, Version 1.0. (See accompanying --- file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) --- - --- Connection system variables -SET NAMES utf8; - --- Database -DROP DATABASE IF EXISTS boost_mysql_order_management; -CREATE DATABASE boost_mysql_order_management; -USE boost_mysql_order_management; - --- User -DROP USER IF EXISTS 'orders_user'@'%'; -CREATE USER 'orders_user'@'%' IDENTIFIED WITH 'mysql_native_password'; -ALTER USER 'orders_user'@'%' IDENTIFIED BY 'orders_password'; -GRANT ALL PRIVILEGES ON boost_mysql_order_management.* TO 'orders_user'@'%'; -FLUSH PRIVILEGES; - --- Table definitions -CREATE TABLE products ( - id INT PRIMARY KEY AUTO_INCREMENT, - short_name VARCHAR(100) NOT NULL, - descr TEXT, - price INT NOT NULL, - FULLTEXT(short_name, descr) -); - -CREATE TABLE orders( - id INT PRIMARY KEY AUTO_INCREMENT, - `status` ENUM('draft', 'pending_payment', 'complete') NOT NULL DEFAULT 'draft' -); - -CREATE TABLE order_items( - id INT PRIMARY KEY AUTO_INCREMENT, - order_id INT NOT NULL, - product_id INT NOT NULL, - quantity INT NOT NULL, - FOREIGN KEY (order_id) REFERENCES orders(id), - FOREIGN KEY (product_id) REFERENCES products(id) -); - --- Procedures -DELIMITER // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE get_products(IN p_search VARCHAR(50)) -BEGIN - DECLARE max_products INT DEFAULT 20; - IF p_search IS NULL THEN - SELECT id, short_name, descr, price - FROM products - LIMIT max_products; - ELSE - SELECT id, short_name, descr, price FROM products - WHERE MATCH(short_name, descr) AGAINST(p_search) - LIMIT max_products; - END IF; -END // - -CREATE PROCEDURE create_order() -BEGIN - START TRANSACTION; - - -- Create the order - INSERT INTO orders () VALUES (); - - -- Return the order - SELECT id, `status` - FROM orders - WHERE id = LAST_INSERT_ID(); - - COMMIT; -END // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE get_order( - IN p_order_id INT -) -BEGIN - DECLARE order_status TEXT; - START TRANSACTION READ ONLY; - - -- Check parameters - IF p_order_id IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'get_order: invalid parameters'; - END IF; - - -- Check that the order exists - SELECT `status` - INTO order_status - FROM orders WHERE id = p_order_id; - IF order_status IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; - END IF; - - -- Return the order. The IFNULL statements make MySQL correctly report the fields as non-NULL - SELECT - IFNULL(p_order_id, 0) AS id, - IFNULL(order_status, 'draft') AS `status`; - SELECT - item.id AS id, - item.quantity AS quantity, - prod.price AS unit_price - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = p_order_id; - - COMMIT; -END // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE get_orders() -BEGIN - SELECT id, `status` FROM orders; -END // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE add_line_item( - IN p_order_id INT, - IN p_product_id INT, - IN p_quantity INT, - OUT pout_line_item_id INT -) -BEGIN - DECLARE product_price INT; - DECLARE order_status TEXT; - START TRANSACTION; - - -- Check parameters - IF p_order_id IS NULL OR p_product_id IS NULL OR p_quantity IS NULL OR p_quantity <= 0 THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'add_line_item: invalid params'; - END IF; - - -- Ensure that the product is valid - SELECT price INTO product_price FROM products WHERE id = p_product_id; - IF product_price IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given product does not exist'; - END IF; - - -- Get the order - SELECT `status` INTO order_status FROM orders WHERE id = p_order_id; - IF order_status IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; - END IF; - IF order_status <> 'draft' THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not editable'; - END IF; - - -- Insert the new item - INSERT INTO order_items (order_id, product_id, quantity) VALUES (p_order_id, p_product_id, p_quantity); - - -- Return value - SET pout_line_item_id = LAST_INSERT_ID(); - - -- Return the edited order - SELECT id, `status` - FROM orders WHERE id = p_order_id; - SELECT - item.id AS id, - item.quantity AS quantity, - prod.price AS unit_price - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = p_order_id; - - COMMIT; -END // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE remove_line_item( - IN p_line_item_id INT -) -BEGIN - DECLARE order_id INT; - DECLARE order_status TEXT; - START TRANSACTION; - - -- Check parameters - IF p_line_item_id IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'remove_line_item: invalid params'; - END IF; - - -- Get the order - SELECT orders.id, orders.`status` - INTO order_id, order_status - FROM orders - JOIN order_items items ON (orders.id = items.order_id) - WHERE items.id = p_line_item_id; - - IF order_status IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order item does not exist'; - END IF; - IF order_status <> 'draft' THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not editable'; - END IF; - - -- Delete the line item - DELETE FROM order_items - WHERE id = p_line_item_id; - - -- Return the edited order - SELECT id, `status` - FROM orders WHERE id = order_id; - SELECT - item.id AS id, - item.quantity AS quantity, - prod.price AS unit_price - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = order_id; - - COMMIT; -END // - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE checkout_order( - IN p_order_id INT, - OUT pout_order_total INT -) -BEGIN - DECLARE order_status TEXT; - START TRANSACTION; - - -- Check parameters - IF p_order_id IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'checkout_order: invalid params'; - END IF; - - -- Get the order - SELECT `status` - INTO order_status - FROM orders WHERE id = p_order_id; - - IF order_status IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; - END IF; - IF order_status <> 'draft' THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be checked out'; - END IF; - - -- Update the order - UPDATE orders SET `status` = 'pending_payment' WHERE id = p_order_id; - - -- Retrieve the total price - SELECT SUM(prod.price * item.quantity) - INTO pout_order_total - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = p_order_id; - - -- Return the edited order - SELECT id, `status` - FROM orders WHERE id = p_order_id; - SELECT - item.id AS id, - item.quantity AS quantity, - prod.price AS unit_price - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = p_order_id; - - COMMIT; -END // - - -CREATE DEFINER = 'orders_user'@'%' PROCEDURE complete_order( - IN p_order_id INT -) -BEGIN - DECLARE order_status TEXT; - START TRANSACTION; - - -- Check parameters - IF p_order_id IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1048, MESSAGE_TEXT = 'complete_order: invalid params'; - END IF; - - -- Get the order - SELECT `status` - INTO order_status - FROM orders WHERE id = p_order_id; - - IF order_status IS NULL THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1329, MESSAGE_TEXT = 'The given order does not exist'; - END IF; - IF order_status <> 'pending_payment' THEN - SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1000, MESSAGE_TEXT = 'The given order is not in a state that can be completed'; - END IF; - - -- Update the order - UPDATE orders SET `status` = 'complete' WHERE id = p_order_id; - - -- Return the edited order - SELECT id, `status` - FROM orders WHERE id = p_order_id; - SELECT - item.id AS id, - item.quantity AS quantity, - prod.price AS unit_price - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = p_order_id; - - COMMIT; -END // - -DELIMITER ; - --- Contents for the products table -INSERT INTO products (price, short_name, descr) VALUES - (6400, 'A Feast for Odin', 'A Feast for Odin is a points-driven game, with plethora of pathways to victory, with a range of risk balanced against reward. A significant portion of this is your central hall, which has a whopping -86 points of squares and a major part of your game is attempting to cover these up with various tiles. Likewise, long halls and island colonies can also offer large rewards, but they will have penalties of their own.'), - (1600, 'Railroad Ink', 'The critically acclaimed roll and write game where you draw routes on your board trying to connect the exits at its edges. The more you connect, the more points you make, but beware: each incomplete route will make you lose points!'), - (4000, 'Catan', 'Catan is a board game for two to four players in which you compete to gather resources and build the biggest settlements on the fictional island of Catan. It takes approximately one hour to play.'), - (2500, 'Not Alone', 'It is the 25th century. You are a member of an intergalactic expedition shipwrecked on a mysterious planet named Artemia. While waiting for the rescue ship, you begin to explore the planet but an alien entity picks up your scent and begins to hunt you. You are NOT ALONE! Will you survive the dangers of Artemia?'), - (4500, 'Dice Hospital', "In Dice Hospital, a worker placement board game, players are tasked with running a local hospital. Each round you'll be admitting new patients, hiring specialists, building new departments, and treating as many incoming patients as you can.") -; diff --git a/example/order_management/parse_cmdline.hpp b/example/order_management/parse_cmdline.hpp deleted file mode 100644 index 61c4914f9..000000000 --- a/example/order_management/parse_cmdline.hpp +++ /dev/null @@ -1,200 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#ifndef BOOST_MYSQL_EXAMPLE_ORDER_MANAGEMENT_PARSE_CMDLINE_HPP -#define BOOST_MYSQL_EXAMPLE_ORDER_MANAGEMENT_PARSE_CMDLINE_HPP - -#include - -#include - -#include -#include -#include - -/** - * Our command line tool implements several sub-commands. Each sub-command - * has a set of arguments. We define a struct for each sub-command. - */ - -struct get_products_args -{ - std::string search; -}; - -struct create_order_args -{ -}; - -struct get_order_args -{ - std::int64_t order_id; -}; - -struct get_orders_args -{ -}; - -struct add_line_item_args -{ - std::int64_t order_id; - std::int64_t product_id; - std::int64_t quantity; -}; - -struct remove_line_item_args -{ - std::int64_t line_item_id; -}; - -struct checkout_order_args -{ - std::int64_t order_id; -}; - -struct complete_order_args -{ - std::int64_t order_id; -}; - -// A variant type that can represent arguments for any of the sub-commands -using any_command = boost::variant2::variant< - get_products_args, - get_order_args, - get_orders_args, - create_order_args, - add_line_item_args, - remove_line_item_args, - checkout_order_args, - complete_order_args>; - -// In-memory representation of the command-line arguments once parsed. -struct cmdline_args -{ - const char* username; - const char* password; - const char* host; - any_command cmd; -}; - -// Call on error to print usage and exit -[[noreturn]] inline void usage(boost::mysql::string_view program_name) -{ - std::cerr << "Usage: " << program_name << " args...\n" - << "Available commands:\n" - " get-products \n" - " create-order\n" - " get-order \n" - " get-orders\n" - " add-line-item \n" - " remove-line-item \n" - " checkout-order \n" - " complete-order " - << std::endl; - exit(1); -} - -// Helper function to parse a sub-command -inline any_command parse_subcommand( - boost::mysql::string_view program_name, - boost::mysql::string_view cmd_name, - int argc_rest, - char** argv_rest -) -{ - if (cmd_name == "get-products") - { - if (argc_rest != 1) - { - usage(program_name); - } - return get_products_args{argv_rest[0]}; - } - else if (cmd_name == "create-order") - { - if (argc_rest != 0) - { - usage(program_name); - } - return create_order_args{}; - } - else if (cmd_name == "get-order") - { - if (argc_rest != 1) - { - usage(program_name); - } - return get_order_args{std::stoi(argv_rest[0])}; - } - else if (cmd_name == "get-orders") - { - if (argc_rest != 0) - { - usage(program_name); - } - return get_orders_args{}; - } - else if (cmd_name == "add-line-item") - { - if (argc_rest != 3) - { - usage(program_name); - } - return add_line_item_args{ - std::stoi(argv_rest[0]), - std::stoi(argv_rest[1]), - std::stoi(argv_rest[2]), - }; - } - else if (cmd_name == "remove-line-item") - { - if (argc_rest != 1) - { - usage(program_name); - } - return remove_line_item_args{ - std::stoi(argv_rest[0]), - }; - } - else if (cmd_name == "checkout-order") - { - if (argc_rest != 1) - { - usage(program_name); - } - return checkout_order_args{std::stoi(argv_rest[0])}; - } - else if (cmd_name == "complete-order") - { - if (argc_rest != 1) - { - usage(program_name); - } - return complete_order_args{std::stoi(argv_rest[0])}; - } - else - { - usage(program_name); - } -} - -// Parses the entire command line -inline cmdline_args parse_cmdline_args(int argc, char** argv) -{ - if (argc < 5) - { - usage(argv[0]); - } - return cmdline_args{ - argv[1], - argv[2], - argv[3], - parse_subcommand(argv[0], argv[4], argc - 5, argv + 5), - }; -} - -#endif diff --git a/example/order_management/prepared_statements_cpp11.cpp b/example/order_management/prepared_statements_cpp11.cpp deleted file mode 100644 index f904a69fa..000000000 --- a/example/order_management/prepared_statements_cpp11.cpp +++ /dev/null @@ -1,437 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_prepared_statements_cpp11 - -/** - * This example implements a very simple command-line order manager - * for an online store, using prepared statements. You can find the table - * definitions in example/order_management/db_setup.sql. Be sure to run this file before the example. - * This example assumes you are connecting to a localhost MySQL server. - * - * The order system is intentionally very simple, and has the following tables: - * - products: the list of items our store sells, with price and description. - * - orders: the main object. Orders have a status field that can be draft, pending_payment or complete. - * - order_items: an order may have 0 to n line items. Each item refers to a single product. - * - * Orders are created empty, in a draft state. Line items can be added or removed. - * Orders are then checked out, which transitions them to pending_payment. - * After that, payment would happen through an external system. Once completed, an - * order is confirmed, transitioning it to the complete status. - * In the real world, flow would be much more complex, but this is enough for an example. - * - * We'll be using the untyped interface to retrieve results from MySQL. - * This makes use of the results, rows_view, row_view and field_view classes. - * If you prefer typing your rows statically, you may prefer using the "typed interface", - * which uses static_results instead. - */ - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -// This header contains boilerplate code to parse the command line -// arguments into structs. Parsing the command line yields a cmdline_args, -// an alias for a boost::variant2::variant holding the command line -// arguments for any of the subcommands. We will use it via visit(). -#include "parse_cmdline.hpp" - -namespace mysql = boost::mysql; - -namespace { - -// This visitor executes a sub-command and prints the results to stdout. -struct visitor -{ - mysql::tcp_ssl_connection& conn; - - // Retrieves an order with its items. If the order exists, at least one record is returned. - // If the order has line items, a record per item is returned. If the order has no items, - // a single record is returned, and it will have its item_xxx fields set to NULL. - mysql::results get_order_with_items(std::int64_t order_id) const - { - // Prepare a statement, since the order_id is provided by the user - mysql::statement stmt = conn.prepare_statement(R"%( - SELECT - ord.id AS order_id, - ord.status AS order_status, - item.id AS item_id, - item.quantity AS item_quantity, - prod.price AS item_unit_price - FROM orders ord - LEFT JOIN order_items item ON ord.id = item.order_id - LEFT JOIN products prod ON item.product_id = prod.id - WHERE ord.id = ? - )%"); - - // Execute it - mysql::results result; - conn.execute(stmt.bind(order_id), result); - - return result; - } - - // Prints an order with its line items to stdout. Each row has the format - // described in get_order_with_items - static void print_order_with_items(mysql::rows_view ord_items) - { - // Print the order - std::cout << "Order: id=" << ord_items.at(0).at(0) << ", status=" << ord_items.at(0).at(1) << '\n'; - - // Print the items (3rd to 5th fields). These will be NULL if the order - // contains no items. - if (ord_items.at(0).at(2).is_null()) - { - std::cout << "No line items\n"; - } - else - { - for (mysql::row_view item : ord_items) - { - std::cout << " Line item: id=" << item.at(2) << ", quantity=" << item.at(3) - << ", unit_price=" << item.at(4).as_int64() / 100.0 << "$\n"; - } - } - } - - // get-products : full text search of the products table. - // use this command to search the store for available products - void operator()(const get_products_args& args) const - { - // Our SQL contains a user-supplied paremeter (the search term), - // so we will be using a prepared statement - mysql::statement stmt = conn.prepare_statement(R"%( - SELECT id, short_name, descr, price - FROM products - WHERE MATCH(short_name, descr) AGAINST(?) - LIMIT 5 - )%"); - - // Execute it - mysql::results products; - conn.execute(stmt.bind(args.search), products); - - // Print the results to stdout - std::cout << "Your search returned the following products:\n"; - for (mysql::row_view prod : products.rows()) - { - std::cout << "* ID: " << prod.at(0) << '\n' - << " Short name: " << prod.at(1) << '\n' - << " Description: " << prod.at(2) << '\n' - << " Price: " << prod.at(3).as_int64() / 100.0 << "$" << std::endl; - } - std::cout << std::endl; - } - - // create-order: creates a new order. Orders are always created empty. This command - // requires no arguments - void operator()(const create_order_args&) const - { - // Our SQL doesn't have parameters, so we can use a text query. - mysql::results result; - conn.execute("INSERT INTO orders VALUES ()", result); - - // Print the results to stdout. results::last_insert_id() returns the ID of - // the newly inserted order. - std::cout << "Order: id=" << result.last_insert_id() << ", status=draft" << std::endl; - } - - // get-order : retrieves order details - void operator()(const get_order_args& args) const - { - // Retrieve the order with its items - mysql::results result = get_order_with_items(args.order_id); - - // If we didn't find any order, issue an error - if (result.rows().empty()) - { - throw std::runtime_error("Can't find order with id=" + std::to_string(args.order_id)); - } - - // Print the order to stdout - std::cout << "Retrieved order\n"; - print_order_with_items(result.rows()); - } - - // get-orders: lists all orders. Orders are listed without their line items. - void operator()(const get_orders_args&) const - { - // Since this query doesn't have parameters, we don't need a prepared statement, - // and we can use a text query instead. - mysql::results result; - conn.execute("SELECT id, `status` FROM orders", result); - - // Print the results to stdout - if (result.rows().empty()) - { - std::cout << "No orders found" << std::endl; - } - else - { - for (mysql::row_view order : result.rows()) - { - std::cout << "Order: id=" << order.at(0) << ", status=" << order.at(1) << '\n'; - } - } - } - - // add-line-item : adds a line item to a given order - void operator()(const add_line_item_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::results result; - conn.execute("START TRANSACTION", result); - - // To add a line item, we require the order to be in a draft status. Get the order to check this fact. - mysql::statement stmt = conn.prepare_statement("SELECT `status` FROM orders WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), result); - if (result.rows().empty()) - { - // There is no such order - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (result.rows()[0].at(0).as_string() != "draft") - { - // The order is no longer editable - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " is not editable"); - } - - // Insert the new line item. If the given product does not exist, the INSERT will fail - // because of product_id's FOREIGN KEY constraint. - stmt = conn.prepare_statement( - "INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)" - ); - conn.execute(stmt.bind(args.order_id, args.product_id, args.quantity), result); - - // We can use results::last_insert_id to get the ID of the new line item. - auto new_line_item_id = result.last_insert_id(); - - // Retrieve the full order details - mysql::results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", result); - - // Print the results to stdout - std::cout << "Created line item: id=" << new_line_item_id << "\n"; - print_order_with_items(order_results.rows()); - } - - // remove-line-item : removes an item from an order - void operator()(const remove_line_item_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::results result; - conn.execute("START TRANSACTION", result); - - // To remove a line item, we require the order to be in a draft status. Get the order to check it. - auto stmt = conn.prepare_statement(R"%( - SELECT orders.id, orders.`status` - FROM orders - JOIN order_items items ON (orders.id = items.order_id) - WHERE items.id = ? - )%"); - conn.execute(stmt.bind(args.line_item_id), result); - if (result.rows().empty()) - { - // The query hasn't matched any row - the supplied line item ID is not valid - throw std::runtime_error( - "The order item with id=" + std::to_string(args.line_item_id) + " does not exist" - ); - } - mysql::row_view order = result.rows()[0]; - if (order.at(1).as_string() != "draft") - { - // The order is no longer editable - throw std::runtime_error("The order is not in an editable state"); - } - - // Remove the line item - stmt = conn.prepare_statement("DELETE FROM order_items WHERE id = ?"); - conn.execute(stmt.bind(args.line_item_id), result); - - // Retrieve the full order details - mysql::results order_results = get_order_with_items(order.at(0).as_int64()); - - // We're done - commit the transaction - conn.execute("COMMIT", result); - - // Print results to stdout - std::cout << "Removed line item from order\n"; - print_order_with_items(order_results.rows()); - } - - // checkout-order : marks an order as ready for checkout - void operator()(const checkout_order_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::results result; - conn.execute("START TRANSACTION", result); - - // To checkout an order, we require it to be in a draft status. Check this fact. - mysql::statement stmt = conn.prepare_statement("SELECT `status` FROM orders WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), result); - if (result.rows().empty()) - { - // No order matched - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (result.rows()[0].at(0).as_string() != "draft") - { - // The order is no longer editable - throw std::runtime_error( - "Order with id=" + std::to_string(args.order_id) + " cannot be checked out" - ); - } - - // Update the order status - stmt = conn.prepare_statement("UPDATE orders SET `status` = 'pending_payment' WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), result); - - // Calculate the total amount to pay. SUM() returns a DECIMAL, which has a bigger - // range than integers. DECIMAL is represented in C++ as a string. We use CAST to obtain - // an uint64_t. If the CAST overflows, the max value for uint64_t will be returned. - // We will be limiting our orders to USD 1bn, so overflow will be detected. - stmt = conn.prepare_statement(R"%( - SELECT CAST( - IFNULL(SUM(prod.price * item.quantity), 0) - AS UNSIGNED - ) - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = ?; - )%"); - conn.execute(stmt.bind(args.order_id), result); - std::uint64_t total_amount = result.rows().at(0).at(0).as_uint64(); - - // Verify that the total amount meets our criteria - if (total_amount == 0) - { - throw std::runtime_error("The order doesn't have any line item"); - } - else if (total_amount > 1000 * 1000 * 100) - { - throw std::runtime_error("Order amount of " + std::to_string(total_amount) + " exceeds limit"); - } - - // Retrieve the full order details - mysql::results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", result); - - // Print the results to stdout - std::cout << "Checked out order. The total amount to pay is: " << total_amount / 100.0 << "$\n"; - print_order_with_items(order_results.rows()); - } - - // complete-order : marks an order as completed - void operator()(const complete_order_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::results result; - conn.execute("START TRANSACTION", result); - - // To complete an order, we require it to be in a pending_payment status. Check this fact. - auto stmt = conn.prepare_statement("SELECT `status` FROM orders WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), result); - if (result.rows().empty()) - { - // Order not found - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (result.rows()[0].at(0).as_string() != "pending_payment") - { - throw std::runtime_error( - "Order with id=" + std::to_string(args.order_id) + " is not in pending_payment status" - ); - } - - // Update status - stmt = conn.prepare_statement("UPDATE orders SET `status` = 'complete' WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), result); - - // Retrieve the full order details - mysql::results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", result); - - // Print the results to stdout - std::cout << "Completed order\n"; - print_order_with_items(order_results.rows()); - } -}; - -void main_impl(int argc, char** argv) -{ - // Parse command line arguments - auto args = parse_cmdline_args(argc, argv); - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // Connection params - mysql::handshake_params params( - args.username, // username - args.password, // password - "boost_mysql_order_management" // database to use - ); - - // Hostname resolution - auto endpoints = resolver.resolve(args.host, mysql::default_port_string); - - // TCP and MySQL level connect - conn.connect(*endpoints.begin(), params); - - // Execute the command - boost::variant2::visit(visitor{conn}, args.cmd); - - // Close the connection - conn.close(); -} - -} // namespace - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's encoding - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << ", error code: " << err.code() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/order_management/prepared_statements_cpp14.cpp b/example/order_management/prepared_statements_cpp14.cpp deleted file mode 100644 index d3d3ffe3a..000000000 --- a/example/order_management/prepared_statements_cpp14.cpp +++ /dev/null @@ -1,538 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_prepared_statements_cpp14 - -/** - * This example implements a very simple command-line order manager - * for an online store, using prepared statements. You can find the table - * definitions in example/order_management/db_setup.sql. Be sure to run this file before the example. - * This example assumes you are connecting to a localhost MySQL server. - * - * The order system is intentionally very simple, and has the following tables: - * - products: the list of items our store sells, with price and description. - * - orders: the main object. Orders have a status field that can be draft, pending_payment or complete. - * - order_items: an order may have 0 to n line items. Each item refers to a single product. - * - * Orders are created empty, in a draft state. Line items can be added or removed. - * Orders are then checked out, which transitions them to pending_payment. - * After that, payment would happen through an external system. Once completed, an - * order is confirmed, transitioning it to the complete status. - * In the real world, flow would be much more complex, but this is enough for an example. - * - * We'll be using the static interface to retrieve results from MySQL. - * This makes use of the static_results class template. - * To use it, we need to define a set of structs/tuples describing the shape - * of our rows. Boost.MySQL will parse the received rows into these types. - * The static interface requires C++14 to work. - * - * Row types may be plain structs or std::tuple's. If we use plain structs, we need - * to use BOOST_DESCRIBE_STRUCT on them. This adds the structs the required reflection - * data, so Boost.MySQL knows how to parse rows into them. - */ - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -// This header contains boilerplate code to parse the command line -// arguments into structs. Parsing the command line yields a cmdline_args, -// an alias for a boost::variant2::variant holding the command line -// arguments for any of the subcommands. We will use it via visit(). -#include "parse_cmdline.hpp" - -// Including any of the static interface headers brings this macro into -// scope if the static interface is supported. -#ifdef BOOST_MYSQL_CXX14 - -namespace mysql = boost::mysql; - -namespace { - -// An order retrieved by our system. -struct order -{ - // The unique database ID of the object. - std::int64_t id; - - // The order status (draft, pending_payment, complete). - std::string status; -}; -BOOST_DESCRIBE_STRUCT(order, (), (id, status)) - -// A product, as listed in the store product catalog. -struct product -{ - // The unique database ID of the object. - std::int64_t id; - // A short name for the product. Can be used as a title. - std::string short_name; - - // The product's description. This field can be NULL in the DB, - // so we use boost::optional for it. If you're using C++17 or higher, - // you can use std::optional instead. - boost::optional descr; - - // The product's unit price, in cents of USD. - std::int64_t price; -}; -BOOST_DESCRIBE_STRUCT(product, (), (id, short_name, descr, price)) - -// An order with its line items. This record type is returned by JOINs -// from the orders and order_items tables. We use this type to retrieve both -// an order and its line items in a single operation. -// If the order contains no line items, the item_xxx fields are NULL. -struct order_with_items -{ - // The ID of the order - std::int64_t order_id; - - // The status of the order - std::string order_status; - - // The ID of the line item, or NULL if the order doesn't have any - boost::optional item_id; - - // The number of units of this product that the user wants to buy, - // or NULL if the order doesn't have line items - boost::optional item_quantity; - - // The product's unit price, in cents of USD, or NULL if the order - // doesn't have line items - boost::optional item_unit_price; - - bool has_item() const - { - return item_id.has_value() && item_quantity.has_value() && item_unit_price.has_value(); - } -}; -BOOST_DESCRIBE_STRUCT( - order_with_items, - (), - (order_id, order_status, item_id, item_quantity, item_unit_price) -); - -// An empty row type. This can be used to describe empty resultsets, -// like the ones returned by INSERT or CALL. -using empty = std::tuple<>; - -// This visitor executes a sub-command and prints the results to stdout. -struct visitor -{ - mysql::tcp_ssl_connection& conn; - - static void print_order(const order& ord) - { - std::cout << "Order: id=" << ord.id << ", status=" << ord.status << '\n'; - } - - // Retrieves an order with its items. If the order exists, at least one record is returned. - // If the order has line items, a record per item is returned. If the order has no items, - // a single record is returned, and it will have its item_xxx fields set to NULL. - mysql::static_results get_order_with_items(std::int64_t order_id) const - { - mysql::statement stmt = conn.prepare_statement(R"%( - SELECT - ord.id AS order_id, - ord.status AS order_status, - item.id AS item_id, - item.quantity AS item_quantity, - prod.price AS item_unit_price - FROM orders ord - LEFT JOIN order_items item ON ord.id = item.order_id - LEFT JOIN products prod ON item.product_id = prod.id - WHERE ord.id = ? - )%"); - - mysql::static_results result; - conn.execute(stmt.bind(order_id), result); - return result; - } - - // Prints an order with its line items to stdout - static void print_order_with_items(boost::span ord_items) - { - assert(!ord_items.empty()); - - // Print the order - std::cout << "Order: id=" << ord_items[0].order_id << ", status=" << ord_items[0].order_status - << '\n'; - - // Print the items - if (!ord_items[0].has_item()) - { - std::cout << "No line items\n"; - } - else - { - for (const auto& item : ord_items) - { - std::cout << " Line item: id=" << *item.item_id << ", quantity=" << *item.item_quantity - << ", unit_price=" << *item.item_unit_price / 100.0 << "$\n"; - } - } - } - - // get-products : full text search of the products table. - // use this command to search the store for available products - void operator()(const get_products_args& args) const - { - // Our SQL contains a user-supplied paremeter (the search term), - // so we will be using a prepared statement - mysql::statement stmt = conn.prepare_statement(R"%( - SELECT id, short_name, descr, price - FROM products - WHERE MATCH(short_name, descr) AGAINST(?) - LIMIT 5 - )%"); - - // The product struct describes the shape of the rows that - // we expect the server to send. - mysql::static_results products; - conn.execute(stmt.bind(args.search), products); - - // Print the results to stdout - std::cout << "Your search returned the following products:\n"; - for (const product& prod : products.rows()) - { - std::cout << "* ID: " << prod.id << '\n' - << " Short name: " << prod.short_name << '\n' - << " Description: " << (prod.descr ? *prod.descr : "") << '\n' - << " Price: " << prod.price / 100.0 << "$" << std::endl; - } - std::cout << std::endl; - } - - // create-order: creates a new order. Orders are always created empty. This command - // requires no arguments - void operator()(const create_order_args&) const - { - // Since this is an INSERT, we don't expect any row to be returned. - // empty is an alias for std::tuple<>, which tells static_results to expect - // an empty resultset. - mysql::static_results result; - conn.execute("INSERT INTO orders VALUES ()", result); - - // We can use static_results::last_insert_id() to retrieve the ID of the newly - // created object. last_insert_id() returns always a uint64_t. Our schema uses - // plain INTs for the id field, so this cast is safe. - order ord{static_cast(result.last_insert_id()), "draft"}; - print_order(ord); - } - - // get-order : retrieves order details - void operator()(const get_order_args& args) const - { - // Retrieve the order with its items - mysql::static_results result = get_order_with_items(args.order_id); - - // If we didn't find any order, issue an error - if (result.rows().empty()) - { - throw std::runtime_error("Can't find order with id=" + std::to_string(args.order_id)); - } - - // Print the order to stdout - std::cout << "Retrieved order\n"; - print_order_with_items(result.rows()); - } - - // get-orders: lists all orders. Orders are listed without their line items. - void operator()(const get_orders_args&) const - { - // Since this query doesn't have parameters, we don't need a prepared statement, - // and we can use a text query instead. - mysql::static_results result; - conn.execute("SELECT id, `status` FROM orders", result); - - // Print the results to stdout - if (result.rows().empty()) - { - std::cout << "No orders found" << std::endl; - } - else - { - for (const order& ord : result.rows()) - { - print_order(ord); - } - } - } - - // add-line-item : adds a line item to a given order - void operator()(const add_line_item_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::static_results empty_results; - conn.execute("START TRANSACTION", empty_results); - - // To add a line item, we require the order to be in a draft status. Get the order to check this fact. - mysql::statement stmt = conn.prepare_statement("SELECT id, `status` FROM orders WHERE id = ?"); - mysql::static_results orders; - conn.execute(stmt.bind(args.order_id), orders); - if (orders.rows().empty()) - { - // There is no such order - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (orders.rows()[0].status != "draft") - { - // The order is no longer editable - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " is not editable"); - } - - // Insert the new line item. If the given product does not exist, the INSERT will fail - // because of product_id's FOREIGN KEY constraint. - stmt = conn.prepare_statement( - "INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)" - ); - conn.execute(stmt.bind(args.order_id, args.product_id, args.quantity), empty_results); - - // We can use static_results::last_insert_id to get the ID of the new line item. - auto new_line_item_id = empty_results.last_insert_id(); - - // Retrieve the full order details - mysql::static_results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", empty_results); - - // Print the results to stdout - std::cout << "Created line item: id=" << new_line_item_id << "\n"; - print_order_with_items(order_results.rows()); - } - - // remove-line-item : removes an item from an order - void operator()(const remove_line_item_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::static_results empty_results; - conn.execute("START TRANSACTION", empty_results); - - // To remove a line item, we require the order to be in a draft status. Get the order to check this - // fact. - mysql::static_results orders; - auto stmt = conn.prepare_statement(R"%( - SELECT orders.id, orders.`status` - FROM orders - JOIN order_items items ON (orders.id = items.order_id) - WHERE items.id = ? - )%"); - conn.execute(stmt.bind(args.line_item_id), orders); - if (orders.rows().empty()) - { - // The query hasn't matched any row - the supplied line item ID is not valid - throw std::runtime_error( - "The order item with id=" + std::to_string(args.line_item_id) + " does not exist" - ); - } - const order& ord = orders.rows()[0]; - if (ord.status != "draft") - { - // The order is no longer editable - throw std::runtime_error("The order is not in an editable state"); - } - - // Remove the line item - stmt = conn.prepare_statement("DELETE FROM order_items WHERE id = ?"); - conn.execute(stmt.bind(args.line_item_id), empty_results); - - // Retrieve the full order details - mysql::static_results order_results = get_order_with_items(ord.id); - - // We're done - commit the transaction - conn.execute("COMMIT", empty_results); - - // Print results to stdout - std::cout << "Removed line item from order\n"; - print_order_with_items(order_results.rows()); - } - - // checkout-order : marks an order as ready for checkout - void operator()(const checkout_order_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::static_results empty_results; - conn.execute("START TRANSACTION", empty_results); - - // To checkout an order, we require it to be in a draft status. Check this fact. - mysql::statement stmt = conn.prepare_statement("SELECT id, `status` FROM orders WHERE id = ?"); - mysql::static_results orders; - conn.execute(stmt.bind(args.order_id), orders); - if (orders.rows().empty()) - { - // No order matched - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (orders.rows()[0].status != "draft") - { - // The order is no longer editable - throw std::runtime_error( - "Order with id=" + std::to_string(args.order_id) + " cannot be checked out" - ); - } - - // Update the order status - stmt = conn.prepare_statement("UPDATE orders SET `status` = 'pending_payment' WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), empty_results); - - // Calculate the total amount to pay. SUM() returns a DECIMAL, which has a bigger - // range than integers. DECIMAL is represented in C++ as a string. We use CAST to obtain - // an uint64_t. If the CAST overflows, the max value for uint64_t will be returned. - // We will be limiting our orders to USD 1bn, so overflow will be detected. - stmt = conn.prepare_statement(R"%( - SELECT CAST( - IFNULL(SUM(prod.price * item.quantity), 0) - AS UNSIGNED - ) - FROM order_items item - JOIN products prod ON item.product_id = prod.id - WHERE item.order_id = ?; - )%"); - mysql::static_results> amount_results; - conn.execute(stmt.bind(args.order_id), amount_results); - std::uint64_t total_amount = std::get<0>(amount_results.rows()[0]); - - // Verify that the total amount meets our criteria - if (total_amount == 0) - { - throw std::runtime_error("The order doesn't have any line item"); - } - else if (total_amount > 1000 * 1000 * 100) - { - throw std::runtime_error("Order amount of " + std::to_string(total_amount) + " exceeds limit"); - } - - // Retrieve the full order details - mysql::static_results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", empty_results); - - // Print the results to stdout - std::cout << "Checked out order. The total amount to pay is: " << total_amount / 100.0 << "$\n"; - print_order_with_items(order_results.rows()); - } - - // complete-order : marks an order as completed - void operator()(const complete_order_args& args) const - { - // We will need to run several statements atomically, so we start a transaction. - mysql::static_results empty_results; - conn.execute("START TRANSACTION", empty_results); - - // To complete an order, we require it to be in a pending_payment status. Check this fact. - auto stmt = conn.prepare_statement("SELECT id, `status` FROM orders WHERE id = ?"); - mysql::static_results orders; - conn.execute(stmt.bind(args.order_id), orders); - if (orders.rows().empty()) - { - // Order not found - throw std::runtime_error("Order with id=" + std::to_string(args.order_id) + " not found"); - } - else if (orders.rows()[0].status != "pending_payment") - { - throw std::runtime_error( - "Order with id=" + std::to_string(args.order_id) + " is not in pending_payment status" - ); - } - - // Update status - stmt = conn.prepare_statement("UPDATE orders SET `status` = 'complete' WHERE id = ?"); - conn.execute(stmt.bind(args.order_id), empty_results); - - // Retrieve the full order details - mysql::static_results order_results = get_order_with_items(args.order_id); - - // We're done - commit the transaction - conn.execute("COMMIT", empty_results); - - // Print the results to stdout - std::cout << "Completed order\n"; - print_order_with_items(order_results.rows()); - } -}; - -void main_impl(int argc, char** argv) -{ - // Parse command line arguments - auto args = parse_cmdline_args(argc, argv); - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // Connection params - mysql::handshake_params params( - args.username, // username - args.password, // password - "boost_mysql_order_management" // database to use - ); - - // Hostname resolution - auto endpoints = resolver.resolve(args.host, mysql::default_port_string); - - // TCP and MySQL level connect - conn.connect(*endpoints.begin(), params); - - // Execute the command - boost::variant2::visit(visitor{conn}, args.cmd); - - // Close the connection - conn.close(); -} - -} // namespace - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's encoding - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << ", 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 - -int main() -{ - std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example" - << std::endl; -} - -#endif - -//] diff --git a/example/order_management/stored_procedures_cpp11.cpp b/example/order_management/stored_procedures_cpp11.cpp deleted file mode 100644 index 367edb8b3..000000000 --- a/example/order_management/stored_procedures_cpp11.cpp +++ /dev/null @@ -1,306 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_stored_procedures - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -/** - * This example implements a very simple command-line order manager - * for an online store, using stored procedures. You can find the procedure - * definitions in example/db_setup_stored_procedures.sql. Be sure to run this file before the example. - * This example assumes you are connecting to a localhost MySQL server. - * - * The order system is intentionally very simple, and has the following tables: - * - products: the list of items our store sells, with price and description. - * - orders: the main object. Orders have a status field that can be draft, pending_payment or complete. - * - order_items: an order may have 0 to n line items. Each item refers to a single product. - * - * Orders are created empty, in a draft state. Line items can be added or removed. - * Orders are then checked out, which transitions them to pending_payment. - * After that, payment would happen through an external system. Once completed, an - * order is confirmed, transitioning it to the complete status. - * In the real world, flow would be much more complex, but this is enough for an example. - * - * We'll be using the untyped interface to retrieve results from MySQL. - * This makes use of the results, rows_view, row_view and field_view classes. - * If you prefer typing your rows statically, you may prefer using the "typed interface", - * which uses static_results instead. - */ - -// This header contains boilerplate code to parse the command line -// arguments into structs. Parsing the command line yields a cmdline_args, -// an alias for a boost::variant2::variant holding the command line -// arguments for any of the subcommands. We will use it via visit(). -#include "parse_cmdline.hpp" - -namespace mysql = boost::mysql; - -namespace { - -// This visitor executes a sub-command and prints the results to stdout. -struct visitor -{ - mysql::tcp_ssl_connection& conn; - - // Prints the details of an order to stdout. An order here is represented as a row - static void print_order(mysql::row_view order) - { - std::cout << "Order: id=" << order.at(0) << ", status=" << order.at(1) << '\n'; - } - - // Prints the details of an order line item, again represented as a row - static void print_line_item(mysql::row_view item) - { - std::cout << " Line item: id=" << item.at(0) << ", quantity=" << item.at(1) - << ", unit_price=" << item.at(2).as_int64() / 100.0 << "$\n"; - } - - // Procedures that manipulate orders return two resultsets: one describing - // the order and another with the line items the order has. Some of them - // return only the order resultset. These functions print order details to stdout - static void print_order_with_items( - mysql::resultset_view order_resultset, - mysql::resultset_view line_items_resultset - ) - { - // First resultset: order information. Always a single row - print_order(order_resultset.rows().at(0)); - - // Second resultset: all order line items - mysql::rows_view line_items = line_items_resultset.rows(); - if (line_items.empty()) - { - std::cout << "No line items\n"; - } - else - { - for (mysql::row_view item : line_items) - { - print_line_item(item); - } - } - } - - // get-products : full text search of the products table - void operator()(const get_products_args& args) const - { - // We need to pass user-supplied params to CALL, so we use a statement - auto stmt = conn.prepare_statement("CALL get_products(?)"); - - mysql::results result; - conn.execute(stmt.bind(args.search), result); - auto products = result.front(); - std::cout << "Your search returned the following products:\n"; - for (auto product : products.rows()) - { - std::cout << "* ID: " << product.at(0) << '\n' - << " Short name: " << product.at(1) << '\n' - << " Description: " << product.at(2) << '\n' - << " Price: " << product.at(3).as_int64() / 100.0 << "$" << std::endl; - } - std::cout << std::endl; - } - - // create-order: creates a new order - void operator()(const create_order_args&) const - { - // Since create_order doesn't have user-supplied params, we can use a text query - mysql::results result; - conn.execute("CALL create_order()", result); - - // Print the result to stdout. create_order() returns a resultset for - // the newly created order, with only 1 row. - std::cout << "Created order\n"; - print_order(result.at(0).rows().at(0)); - } - - // get-order : retrieves order details - void operator()(const get_order_args& args) const - { - // The order_id is supplied by the user, so we use a prepared statement - auto stmt = conn.prepare_statement("CALL get_order(?)"); - - // Execute the statement - mysql::results result; - conn.execute(stmt.bind(args.order_id), result); - - // Print the result to stdout. get_order() returns a resultset for - // the retrieved order and another for the line items. If the order can't - // be found, get_order() raises an error using SIGNAL, which will make - // execute() fail with an exception. - std::cout << "Retrieved order\n"; - print_order_with_items(result.at(0), result.at(1)); - } - - // get-orders: lists all orders - void operator()(const get_orders_args&) const - { - // Since get_orders doesn't have user-supplied params, we can use a text query - mysql::results result; - conn.execute("CALL get_orders()", result); - - // Print results to stdout. get_orders() succeeds even if no order is found. - // get_orders() only lists orders, not line items. - mysql::rows_view orders = result.front().rows(); - if (orders.empty()) - { - std::cout << "No orders found" << std::endl; - } - else - { - for (mysql::row_view order : result.front().rows()) - { - print_order(order); - } - } - } - - // add-line-item : adds a line item to a given order - void operator()(const add_line_item_args& args) const - { - // add_line_item has several user-supplied arguments, so we must use a statement. - // The 4th argument is an OUT parameter. If we bind it by passing a ? marker, - // we will get an extra resultset with just its value. - auto stmt = conn.prepare_statement("CALL add_line_item(?, ?, ?, ?)"); - - // We still have to pass a value to the 4th argument, even if it's an OUT parameter. - // The value will be ignored, so we can pass nullptr. - mysql::results result; - conn.execute(stmt.bind(args.order_id, args.product_id, args.quantity, nullptr), result); - - // We can use results::out_params() to access the extra resultset containing - // the OUT parameter - auto new_line_item_id = result.out_params().at(0).as_int64(); - - // Print the results to stdout - std::cout << "Created line item: id=" << new_line_item_id << "\n"; - print_order_with_items(result.at(0), result.at(1)); - } - - // remove-line-item : removes an item from an order - void operator()(const remove_line_item_args& args) const - { - // remove_line_item has user-supplied parameters, so we use a statement - auto stmt = conn.prepare_statement("CALL remove_line_item(?)"); - - // Run the procedure - mysql::results result; - conn.execute(stmt.bind(args.line_item_id), result); - - // Print results to stdout - std::cout << "Removed line item from order\n"; - print_order_with_items(result.at(0), result.at(1)); - } - - // checkout-order : marks an order as ready for checkout - void operator()(const checkout_order_args& args) const - { - // checkout_order has user-supplied parameters, so we use a statement. - // The 2nd parameter represents the total order amount and is an OUT parameter. - auto stmt = conn.prepare_statement("CALL checkout_order(?, ?)"); - - // Execute the statement - mysql::results result; - conn.execute(stmt.bind(args.order_id, nullptr), result); - - // We can use results::out_params() to access the extra resultset containing - // the OUT parameter - auto total_amount = result.out_params().at(0).as_int64(); - - // Print the results to stdout - std::cout << "Checked out order. The total amount to pay is: " << total_amount / 100.0 << "$\n"; - print_order_with_items(result.at(0), result.at(1)); - } - - // complete-order : marks an order as completed - void operator()(const complete_order_args& args) const - { - // complete_order has user-supplied parameters, so we use a statement. - auto stmt = conn.prepare_statement("CALL complete_order(?)"); - - // Execute the statement - mysql::results result; - conn.execute(stmt.bind(args.order_id), result); - - // Print the results to stdout - std::cout << "Completed order\n"; - print_order_with_items(result.at(0), result.at(1)); - } -}; - -void main_impl(int argc, char** argv) -{ - // Parse command line arguments - auto args = parse_cmdline_args(argc, argv); - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // Connection params - mysql::handshake_params params( - args.username, // username - args.password, // password - "boost_mysql_order_management" // database to use - ); - - // Hostname resolution - auto endpoints = resolver.resolve(args.host, mysql::default_port_string); - - // TCP and MySQL level connect - conn.connect(*endpoints.begin(), params); - - // Execute the command - boost::variant2::visit(visitor{conn}, args.cmd); - - // Close the connection - conn.close(); -} - -} // namespace - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // If a store procedure fails (e.g. because a SIGNAL statement was executed), an error - // like this will be raised. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's encoding - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << ", error code: " << err.code() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] \ No newline at end of file diff --git a/example/order_management/stored_procedures_cpp14.cpp b/example/order_management/stored_procedures_cpp14.cpp deleted file mode 100644 index 27dd64c94..000000000 --- a/example/order_management/stored_procedures_cpp14.cpp +++ /dev/null @@ -1,401 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_stored_procedures_cpp14 - -/** - * This example implements a very simple command-line order manager - * for an online store, using stored procedures. You can find the procedure - * definitions in example/db_setup_stored_procedures.sql. Be sure to run this file before the example. - * This example assumes you are connecting to a localhost MySQL server. - * - * The order system is intentionally very simple, and has the following tables: - * - products: the list of items our store sells, with price and description. - * - orders: the main object. Orders have a status field that can be draft, pending_payment or complete. - * - order_items: an order may have 0 to n line items. Each item refers to a single product. - * - * Orders are created empty, in a draft state. Line items can be added or removed. - * Orders are then checked out, which transitions them to pending_payment. - * After that, payment would happen through an external system. Once completed, an - * order is confirmed, transitioning it to the complete status. - * In the real world, flow would be much more complex, but this is enough for an example. - * - * We'll be using the static interface to retrieve results from MySQL. - * This makes use of the static_results class template. - * To use it, we need to define a set of structs/tuples describing the shape - * of our rows. Boost.MySQL will parse the received rows into these types. - * The static interface requires C++14 to work. - * - * Row types may be plain structs or std::tuple's. If we use plain structs, we need - * to use BOOST_DESCRIBE_STRUCT on them. This adds the structs the required reflection - * data, so Boost.MySQL knows how to parse rows into them. - */ - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include - -// This header contains boilerplate code to parse the command line -// arguments into structs. Parsing the command line yields a cmdline_args, -// an alias for a boost::variant2::variant holding the command line -// arguments for any of the subcommands. We will use it via visit(). -#include "parse_cmdline.hpp" - -// Including any of the static interface headers brings this macro into -// scope if the static interface is supported. -#ifdef BOOST_MYSQL_CXX14 - -namespace mysql = boost::mysql; - -namespace { - -// An order retrieved by our system. -struct order -{ - // The unique database ID of the object. - std::int64_t id; - - // The order status (draft, pending_payment, complete). - std::string status; -}; -BOOST_DESCRIBE_STRUCT(order, (), (id, status)) - -// A line item, associated to an order and to a product. -// Our queries don't retrieve the order or product ID, so -// we don't include them in this struct. -struct order_item -{ - // The unique database ID of the object. - std::int64_t id; - - // The number of units of this product that the user wants to buy. - std::int64_t quantity; - - // The product's unit price, in cents of USD. - std::int64_t unit_price; -}; -BOOST_DESCRIBE_STRUCT(order_item, (), (id, quantity, unit_price)) - -// A product, as listed in the store product catalog. -struct product -{ - // The unique database ID of the object. - std::int64_t id; - - // A short name for the product. Can be used as a title. - std::string short_name; - - // The product's description. This field can be NULL in the DB, - // so we use boost::optional for it. If you're using C++17 or higher, - // you can use std::optional instead. - boost::optional descr; - - // The product's unit price, in cents of USD. - std::int64_t price; -}; -BOOST_DESCRIBE_STRUCT(product, (), (id, short_name, descr, price)) - -// An empty row type. This can be used to describe empty resultsets, -// like the ones returned by INSERT or CALL. -using empty = std::tuple<>; - -// This visitor executes a sub-command and prints the results to stdout. -struct visitor -{ - mysql::tcp_ssl_connection& conn; - - // Prints the details of an order to stdout - static void print_order(const order& ord) - { - std::cout << "Order: id=" << ord.id << ", status=" << ord.status << '\n'; - } - - // Prints the details of an order line item - static void print_line_item(const order_item& item) - { - std::cout << " Line item: id=" << item.id << ", quantity=" << item.quantity - << ", unit_price=" << item.unit_price / 100.0 << "$\n"; - } - - // Prints an order with its line items to stdout - static void print_order_with_items(const order& ord, boost::span items) - { - print_order(ord); - - if (items.empty()) - { - std::cout << "No line items\n"; - } - else - { - for (const auto& item : items) - { - print_line_item(item); - } - } - } - - // get-products : full text search of the products table. - // use this command to search the store for available products - void operator()(const get_products_args& args) const - { - // We need to pass user-supplied params to CALL, so we use a statement - auto stmt = conn.prepare_statement("CALL get_products(?)"); - - // get_products returns two resultsets: - // 1. A collection of products - // 2. An empty resultset describing the effects of the CALL statement - mysql::static_results products; - conn.execute(stmt.bind(args.search), products); - - // Print the results to stdout. By default, rows() returns the rows for the 1st resultset. - std::cout << "Your search returned the following products:\n"; - for (const product& prod : products.rows()) - { - std::cout << "* ID: " << prod.id << '\n' - << " Short name: " << prod.short_name << '\n' - << " Description: " << (prod.descr ? *prod.descr : "") << '\n' - << " Price: " << prod.price / 100.0 << "$" << std::endl; - } - std::cout << std::endl; - } - - // create-order: creates a new order. Orders are always created empty. This command - // requires no arguments - void operator()(const create_order_args&) const - { - // Since create_order doesn't have user-supplied params, we can use a text query. - // create_order returns two resultsets: - // 1. The created order. This is always a single row. - // 2. An empty resultset describing the effects of the CALL statement - mysql::static_results result; - conn.execute("CALL create_order()", result); - - // Print the result to stdout. create_order() returns a resultset for - // the newly created order, with only 1 row. - std::cout << "Created order\n"; - print_order(result.rows()[0]); - } - - // get-order : retrieves order details - void operator()(const get_order_args& args) const - { - // The order_id is supplied by the user, so we use a prepared statement - auto stmt = conn.prepare_statement("CALL get_order(?)"); - - // get_order returns three resultsets: - // 1. The retrieved order. This is always a single row. - // 2. A collection of line items for this order. - // 3. An empty resultset describing the effects of the CALL statement - // If the order can't be found, get_order() raises an error using SIGNAL, which will make - // execute() fail with an exception. - mysql::static_results result; - conn.execute(stmt.bind(args.order_id), result); - - // Print the result to stdout. - // rows() can be used to access rows for the i-th resultset. - // rows() means rows<0>(). - std::cout << "Retrieved order\n"; - print_order_with_items(result.rows<0>()[0], result.rows<1>()); - } - - // get-orders: lists all orders - void operator()(const get_orders_args&) const - { - // Since get_orders doesn't have user-supplied params, we can use a text query - // get_orders returns two resultsets: - // 1. A collection of orders. - // 2. An empty resultset describing the effects of the CALL statement - mysql::static_results result; - conn.execute("CALL get_orders()", result); - - // Print results to stdout. get_orders() succeeds even if no order is found. - // get_orders() only lists orders, not line items. - if (result.rows().empty()) - { - std::cout << "No orders found" << std::endl; - } - else - { - for (const order& ord : result.rows()) - { - print_order(ord); - } - } - } - - // add-line-item : adds a line item to a given order - void operator()(const add_line_item_args& args) const - { - // add_line_item has several user-supplied arguments, so we must use a statement. - // The 4th argument is an OUT parameter. If we bind it by passing a ? marker, - // we will get an extra resultset with just its value. - auto stmt = conn.prepare_statement("CALL add_line_item(?, ?, ?, ?)"); - - // add_line_item returns four resultsets: - // 1. The affected order. Always a single row. - // 2. A collection of line items for the affected order. - // 3. An OUT params resultset, containing the ID of the newly created line item. Single row. - // MySQL always marks OUT params as nullable. - // 4. An empty resultset describing the effects of the CALL statement - using out_params_t = std::tuple>; - mysql::static_results result; - - // We still have to pass a value to the 4th argument, even if it's an OUT parameter. - // The value will be ignored, so we can pass nullptr. - conn.execute(stmt.bind(args.order_id, args.product_id, args.quantity, nullptr), result); - - // We can access the OUT param as we access any other resultset - auto new_line_item_id = std::get<0>(result.rows<2>()[0]).value(); - - // Print the results to stdout - std::cout << "Created line item: id=" << new_line_item_id << "\n"; - print_order_with_items(result.rows<0>()[0], result.rows<1>()); - } - - // remove-line-item : removes an item from an order - void operator()(const remove_line_item_args& args) const - { - // remove_line_item has user-supplied parameters, so we use a statement - auto stmt = conn.prepare_statement("CALL remove_line_item(?)"); - - // remove_line_item returns three resultsets: - // 1. The affected order. Always a single row. - // 2. A collection of line items for the affected order. - // 3. An empty resultset describing the effects of the CALL statement - mysql::static_results result; - conn.execute(stmt.bind(args.line_item_id), result); - - // Print results to stdout - std::cout << "Removed line item from order\n"; - print_order_with_items(result.rows<0>()[0], result.rows<1>()); - } - - // checkout-order : marks an order as ready for checkout - void operator()(const checkout_order_args& args) const - { - // checkout_order has user-supplied parameters, so we use a statement. - // The 2nd parameter represents the total order amount and is an OUT parameter. - auto stmt = conn.prepare_statement("CALL checkout_order(?, ?)"); - - // checkout_order returns four resultsets: - // 1. The affected order. Always a single row. - // 2. A collection of line items for the affected order. - // 3. An OUT params resultset, containing the total amount to pay, in USD cents. Single row. - // MySQL always marks OUT params as nullable. - // 4. An empty resultset describing the effects of the CALL statement - using out_params_t = std::tuple>; - mysql::static_results result; - conn.execute(stmt.bind(args.order_id, nullptr), result); - - // We can access the OUT param as we access any other resultset - auto total_amount = std::get<0>(result.rows<2>()[0]).value_or(0); - - // Print the results to stdout - std::cout << "Checked out order. The total amount to pay is: " << total_amount / 100.0 << "$\n"; - print_order_with_items(result.rows<0>()[0], result.rows<1>()); - } - - // complete-order : marks an order as completed - void operator()(const complete_order_args& args) const - { - // complete_order has user-supplied parameters, so we use a statement. - auto stmt = conn.prepare_statement("CALL complete_order(?)"); - - // complete_order returns three resultsets: - // 1. The affected order. Always a single row. - // 2. A collection of line items for the affected order. - // 3. An empty resultset describing the effects of the CALL statement - mysql::static_results result; - conn.execute(stmt.bind(args.order_id), result); - - // Print the results to stdout - std::cout << "Completed order\n"; - print_order_with_items(result.rows<0>()[0], result.rows<1>()); - } -}; - -void main_impl(int argc, char** argv) -{ - // Parse command line arguments - auto args = parse_cmdline_args(argc, argv); - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // Connection params - mysql::handshake_params params( - args.username, // username - args.password, // password - "boost_mysql_order_management" // database to use - ); - - // Hostname resolution - auto endpoints = resolver.resolve(args.host, mysql::default_port_string); - - // TCP and MySQL level connect - conn.connect(*endpoints.begin(), params); - - // Execute the command - boost::variant2::visit(visitor{conn}, args.cmd); - - // Close the connection - conn.close(); -} - -} // namespace - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // If a store procedure fails (e.g. because a SIGNAL statement was executed), an error - // like this will be raised. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's encoding - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << ", error code: " << err.code() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -#else - -int main() -{ - std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example" - << std::endl; -} - -#endif - -//] diff --git a/example/pipeline.cpp b/example/pipeline.cpp deleted file mode 100644 index 69f1ca128..000000000 --- a/example/pipeline.cpp +++ /dev/null @@ -1,207 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_pipeline - -// This example demonstrates how to use the pipeline API to prepare, -// execute and close statements in batch. -// It uses asynchronous functions and C++20 coroutines (with boost::asio::co_spawn). -// -// Pipelines are an experimental feature. - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#ifdef BOOST_ASIO_HAS_CO_AWAIT - -namespace asio = boost::asio; - -using boost::mysql::string_view; -using boost::mysql::with_diagnostics; - -// Prepare several statements in batch. -// This is faster than preparing them one by one, as it saves round-trips to the server. -asio::awaitable> batch_prepare( - boost::mysql::any_connection& conn, - boost::span statements -) -{ - // Construct a pipeline request describing the work to be performed. - // There must be one prepare_statement_stage per statement to prepare - boost::mysql::pipeline_request req; - for (auto stmt_sql : statements) - req.add_prepare_statement(stmt_sql); - - // Run the pipeline. - // stage_response is a variant-like type that can hold the response of any stage type. - std::vector pipe_res; - co_await conn.async_run_pipeline(req, pipe_res); - - // If we got here, all statements were prepared successfully. - // pipe_res contains as many elements as statements.size(), holding statement objects - // Extract them into a vector - std::vector res; - res.reserve(statements.size()); - for (const auto& stage_res : pipe_res) - res.push_back(stage_res.get_statement()); - co_return res; -} - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - const char* hostname = argv[3]; - - // The company_id to use when inserting new employees. - // This is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // I/O context - boost::asio::io_context ctx; - - // Connection. Note that the connection's type doesn't depend - // on the transport (TCP or UNIX sockets). - boost::mysql::any_connection conn(ctx); - - // Connection configuration. This contains the server address, - // credentials, and other configuration used during connection establishment. - // Note that, by default, TCP connections will use TLS. connect_params::ssl - // allows disabling it. - boost::mysql::connect_params params; - - // The server address. This can either be a host and port or a UNIX socket path - params.server_address.emplace_host_and_port(hostname); - - // Username to log in as - params.username = argv[1]; - - // Password to use - params.password = argv[2]; - - // Database to use; leave empty or omit for no database - params.database = "boost_mysql_examples"; - - // Spawn a coroutine running the passed function - boost::asio::co_spawn( - ctx.get_executor(), - [&conn, ¶ms, company_id]() -> boost::asio::awaitable { - // Connect to the server. with_diagnostics will turn any thrown exceptions - // into error_with_diagnostics, which contain more info than regular exceptions - co_await conn.async_connect(params); - - // Prepare the statements using the batch prepare function that we previously defined - const std::array stmt_sql{ - "INSERT INTO employee (company_id, first_name, last_name) VALUES (?, ?, ?)", - "INSERT INTO audit_log (msg) VALUES (?)" - }; - std::vector stmts = co_await batch_prepare(conn, stmt_sql); - - // Create a pipeline request to execute them. - // Warning: do NOT include the COMMIT statement in this pipeline. - // COMMIT must only be executed if all the previous statements succeeded. - // In a pipeline, all stages get executed, regardless of the outcome of previous stages. - // We say that COMMIT has a dependency on the result of previous stages. - boost::mysql::pipeline_request req; - req.add_execute("START TRANSACTION") - .add_execute(stmts.at(0), company_id, "Juan", "Lopez") - .add_execute(stmts.at(0), company_id, "Pepito", "Rodriguez") - .add_execute(stmts.at(0), company_id, "Someone", "Random") - .add_execute(stmts.at(1), "Inserted 3 new emplyees"); - std::vector res; - - // Execute the pipeline - co_await conn.async_run_pipeline(req, res); - - // If we got here, all stages executed successfully. - // Since they were execution stages, the response contains a results object. - // Get the IDs of the newly created employees - auto id1 = res.at(1).as_results().last_insert_id(); - auto id2 = res.at(2).as_results().last_insert_id(); - auto id3 = res.at(3).as_results().last_insert_id(); - - // We can now commit our transaction and close the statements. - // Clear the request and populate it again - req.clear(); - req.add_execute("COMMIT").add_close_statement(stmts.at(0)).add_close_statement(stmts.at(1)); - - // Run it - co_await conn.async_run_pipeline(req, res); - - // If we got here, our insertions got committed. - std::cout << "Inserted employees: " << id1 << ", " << id2 << ", " << id3 << std::endl; - - // Notify the MySQL server we want to quit, then close the underlying connection. - co_await conn.async_close(); - }, - // If any exception is thrown in the coroutine body, rethrow it. - [](std::exception_ptr ptr) { - if (ptr) - { - std::rethrow_exception(ptr); - } - } - ); - - // Don't forget to call run()! Otherwise, your program - // will not spawn the coroutine and will do nothing. - ctx.run(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's character set - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -#else - -int main(int, char**) { std::cout << "Sorry, your compiler does not support C++20 coroutines" << std::endl; } - -#endif - -//] diff --git a/example/private/run_stored_procedures.py b/example/private/run_stored_procedures.py deleted file mode 100644 index 8848ee971..000000000 --- a/example/private/run_stored_procedures.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/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) -# - -from subprocess import run, PIPE -import argparse -import re - - -def _parse_order_id(output: str) -> str: - res = re.search(r'Order: id=([0-9]*)', output) - assert res is not None - return res.group(1) - - -def _parse_line_item_id(output: str) -> str: - res = re.search(r'Created line item: id=([0-9]*)', output) - assert res is not None - return res.group(1) - - -class Runner: - def __init__(self, exe: str, host: str) -> None: - self._exe = exe - self._host = host - - def run(self, subcmd: str, *args: str) -> str: - cmdline = [self._exe, 'orders_user', 'orders_password', self._host, subcmd, *args] - print(' + ', cmdline) - res = run(cmdline, check=True, stdout=PIPE) - print(res.stdout.decode()) - return res.stdout.decode() - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('executable') - parser.add_argument('host') - args = parser.parse_args() - - runner = Runner(args.executable, args.host) - - # Some examples require C++14 and produce an apology for C++11 compilers - output = runner.run('get-products', 'feast') - if "your compiler doesn't have the required capabilities to run this example" in output: - print('Example reported unsupported compiler') - exit(0) - - order_id = _parse_order_id(runner.run('create-order')) - runner.run('get-orders') - line_item_id = _parse_line_item_id(runner.run('add-line-item', order_id, '1', '5')) - runner.run('add-line-item', order_id, '2', '2') - runner.run('add-line-item', order_id, '3', '1') - runner.run('remove-line-item', line_item_id) - runner.run('get-order', order_id) - runner.run('checkout-order', order_id) - runner.run('complete-order', order_id) - runner.run('get-orders') - - -if __name__ == '__main__': - main() diff --git a/example/ssl.cpp b/example/ssl.cpp deleted file mode 100644 index 868d59fec..000000000 --- a/example/ssl.cpp +++ /dev/null @@ -1,151 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_ssl - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#define ASSERT(expr) \ - if (!(expr)) \ - { \ - std::cerr << "Assertion failed: " #expr << std::endl; \ - exit(1); \ - } - -// The CA file that signed the server's certificate -constexpr const char CA_PEM[] = R"%(-----BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIUWznm2UoxXw3j7HCcp9PpiayTvFQwDQYJKoZIhvcNAQEL -BQAwQjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAMBgNVBAoM -BW15c3FsMQ4wDAYDVQQDDAVteXNxbDAgFw0yMDA0MDQxNDMwMjNaGA8zMDE5MDgw -NjE0MzAyM1owQjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAM -BgNVBAoMBW15c3FsMQ4wDAYDVQQDDAVteXNxbDCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAN0WYdvsDb+a0TxOGPejcwZT0zvTrf921mmDUlrLN1Z0hJ/S -ydgQCSD7Q+6za4lTFZCXcvs52xvvS2gfC0yXyYLCT/jA4RQRxuF+/+w1gDWEbGk0 -KzEpsBuKrEIvEaVdoS78SxInnW/aegshdrRRocp4JQ6KHsZgkLTxSwPfYSUmMUo0 -cRO0Q/ak3VK8NP13A6ZFvZjrBxjS3cSw9HqilgADcyj1D4EokvfI1C9LrgwgLlZC -XVkjjBqqoMXGGlnXOEK+pm8bU68HM/QvMBkb1Amo8pioNaaYgqJUCP0Ch0iu1nUU -HtsWt6emXv0jANgIW0oga7xcT4MDGN/M+IRWLTECAwEAAaNTMFEwHQYDVR0OBBYE -FNxhaGwf5ePPhzK7yOAKD3VF6wm2MB8GA1UdIwQYMBaAFNxhaGwf5ePPhzK7yOAK -D3VF6wm2MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAoeJCAX -IDCFoAaZoQ1niI6Ac/cds8G8It0UCcFGSg+HrZ0YujJxWIruRCUG60Q2OAbEvn0+ -uRpTm+4tV1Wt92WFeuRyqkomozx0g4CyfsxGX/x8mLhKPFK/7K9iTXM4/t+xQC4f -J+iRmPVsMKQ8YsHYiWVhlOMH9XJQiqERCB2kOKJCH6xkaF2k0GbM2sGgbS7Z6lrd -fsFTOIVx0VxLVsZnWX3byE9ghnDR5jn18u30Cpb/R/ShxNUGIHqRa4DkM5la6uZX -W1fpSW11JBSUv4WnOO0C2rlIu7UJWOROqZZ0OsybPRGGwagcyff2qVRuI2XFvAMk -OzBrmpfHEhF6NDU= ------END CERTIFICATE----- -)%"; - -void print_employee(boost::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) -} - -void main_impl(int argc, char** argv) -{ - if (argc != 4) - { - std::cerr << "Usage: " << argv[0] << " \n"; - exit(1); - } - - // I/O context - boost::asio::io_context ctx; - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // Connection params - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit the parameter for no - // database - ); - - // This context will be used by the underlying SSL stream object. We can - // set up here any SSL-related options, like peer verification or CA - // certificates. We will do these in the next lines. - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - - // Check whether the server's certificate is valid and signed by a trusted CA. - // If it's not, our handshake or connect operation will fail. - ssl_ctx.set_verify_mode(boost::asio::ssl::verify_peer); - - // Load a trusted CA, which was used to sign the server's certificate. - // This will allow the signature verification to succeed in our example. - // You will have to run your MySQL server with the test certificates - // located under $BOOST_MYSQL_ROOT/tools/ssl/ - ssl_ctx.add_certificate_authority(boost::asio::buffer(CA_PEM)); - - // We expect the server certificate's common name to be "mysql". - // If it's not, the certificate will be rejected and handshake or connect will fail. - ssl_ctx.set_verify_callback(boost::asio::ssl::host_name_verification("mysql")); - - // Pass in our SSL context to the connection. Note that we - // can create many connections out of a single context. We need to keep the - // context alive until we finish using the connection. - boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Hostname resolution - auto endpoints = resolver.resolve(argv[3], boost::mysql::default_port_string); - - // Connect to the server. This operation will perform the SSL handshake as part of - // it, and thus will fail if the certificate is found to be invalid. - conn.connect(*endpoints.begin(), params); - - // We can now use the connection as we would normally do. - const char* sql = "SELECT first_name, last_name, salary FROM employee"; - boost::mysql::results result; - conn.execute(sql, result); - - for (auto employee : result.rows()) - { - print_employee(employee); - } - - // Cleanup - conn.close(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's 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; - } -} - -//] diff --git a/example/timeouts.cpp b/example/timeouts.cpp deleted file mode 100644 index 3b3bc13ba..000000000 --- a/example/timeouts.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_timeouts - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -using boost::mysql::error_code; - -void print_employee(boost::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) -} - -/** - * We use Boost.Asio's cancel_after completion token to cancel operations - * after a certain time has elapsed. This is not something specific to Boost.MySQL, and - * can be used with any other asynchronous operation that follows Asio's model. - * If the operation times out, it will fail with a boost::asio::error::operation_aborted - * error code. - * - * If any of the MySQL specific operations result in a timeout, the connection is left - * in an unspecified state. You should close it and re-open it to get it working again. - */ -boost::asio::awaitable coro_main( - boost::mysql::tcp_ssl_connection& conn, - boost::asio::ip::tcp::resolver& resolver, - const boost::mysql::handshake_params& params, - const char* hostname, - const char* company_id -) -{ - using boost::asio::cancel_after; - constexpr std::chrono::seconds timeout(8); - - // Resolve hostname - auto endpoints = co_await resolver - .async_resolve(hostname, boost::mysql::default_port_string, cancel_after(timeout)); - - // Connect to server - co_await conn.async_connect(*endpoints.begin(), params, cancel_after(timeout)); - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - boost::mysql::statement stmt = co_await conn.async_prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", - cancel_after(timeout) - ); - - // Execute the statement - boost::mysql::results result; - co_await conn.async_execute(stmt.bind(company_id), result, cancel_after(timeout)); - - // Print all the obtained rows - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // Notify the MySQL server we want to quit, then close the underlying connection. - co_await conn.async_close(cancel_after(timeout)); -} - -void main_impl(int argc, char** argv) -{ - if (argc != 4 && argc != 5) - { - std::cerr << "Usage: " << argv[0] << " [company-id]\n"; - exit(1); - } - - const char* hostname = argv[3]; - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // I/O context and connection. We use SSL because MySQL 8+ default settings require it. - boost::asio::io_context ctx; - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); - boost::mysql::tcp_ssl_connection conn(ctx, ssl_ctx); - - // Connection parameters - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit for no database - ); - - // Resolver for hostname resolution - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - - // The entry point. We pass in a function returning a boost::asio::awaitable, as required. - boost::asio::co_spawn( - ctx.get_executor(), - [&conn, &resolver, params, hostname, company_id] { - return coro_main(conn, resolver, params, hostname, company_id); - }, - // 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 start the requested operations. - ctx.run(); -} - -#else - -void main_impl(int, char**) -{ - std::cout << "Sorry, your compiler does not support C++20 coroutines" << std::endl; -} - -#endif - -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() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/unix_socket.cpp b/example/unix_socket.cpp deleted file mode 100644 index ffb247649..000000000 --- a/example/unix_socket.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -//[example_unix_socket - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -void print_employee(boost::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) -} - -#define ASSERT(expr) \ - if (!(expr)) \ - { \ - std::cerr << "Assertion failed: " #expr << std::endl; \ - exit(1); \ - } - -/** - * UNIX sockets are only available on, er, UNIX systems. Typedefs for - * UNIX socket-based connections are only available in UNIX systems. - * Check for BOOST_ASIO_HAS_LOCAL_SOCKETS to know if UNIX socket - * typedefs are available in your system. - */ -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - -void main_impl(int argc, char** argv) -{ - if (argc != 3 && argc != 4) - { - std::cerr << "Usage: " << argv[0] << " [] []\n"; - exit(1); - } - - const char* socket_path = argc >= 4 ? argv[3] : "/var/run/mysqld/mysqld.sock"; - - // The company_id whose employees we will be listing. This - // is user-supplied input, and should be treated as untrusted. - const char* company_id = argc == 5 ? argv[4] : "HGS"; - - // Connection parameters that tell us where and how to connect to the MySQL server. - // There are two types of parameters: - // - UNIX-level connection parameters, identifying the UNIX socket to connect to. - // - MySQL level parameters: database credentials and schema to use. - boost::asio::local::stream_protocol::endpoint ep(socket_path); - boost::mysql::handshake_params params( - argv[1], // username - argv[2], // password - "boost_mysql_examples" // database to use; leave empty or omit the parameter for no - // database - ); - - boost::asio::io_context ctx; - - // Connection to the MySQL server, over a UNIX socket. Note that we don't need - // to use SSL when using UNIX sockets because it's restricted to the local machine, - // so MySQL considers it secure, even if it's not encrypted. - boost::mysql::unix_connection conn(ctx); - conn.connect(ep, params); // UNIX socket connect and MySQL handshake - - // We will be using company_id, which is untrusted user input, so we will use a prepared - // statement. - boost::mysql::statement stmt = conn.prepare_statement( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?" - ); - - // Execute the statement - boost::mysql::results result; - conn.execute(stmt.bind(company_id), result); - - // Print employees - for (boost::mysql::row_view employee : result.rows()) - { - print_employee(employee); - } - - // Notify the MySQL server we want to quit, then close the underlying connection. - conn.close(); -} - -#else - -void main_impl(int, char**) { std::cout << "Sorry, your system does not support UNIX sockets" << std::endl; } - -#endif - -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; - } -} - -//] diff --git a/include/boost/mysql/execution_state.hpp b/include/boost/mysql/execution_state.hpp index b2bbe647e..6fa9afac5 100644 --- a/include/boost/mysql/execution_state.hpp +++ b/include/boost/mysql/execution_state.hpp @@ -148,6 +148,9 @@ class execution_state /** * \brief Returns the number of rows affected by the SQL statement associated to this resultset. + * Note that this is NOT the number of matched rows. If a row + * is matched but not affected, it won't be accounted for here. + * * \par Exception safety * No-throw guarantee. * diff --git a/include/boost/mysql/results.hpp b/include/boost/mysql/results.hpp index 0e4993695..d15f18de1 100644 --- a/include/boost/mysql/results.hpp +++ b/include/boost/mysql/results.hpp @@ -192,6 +192,9 @@ class results /** * \brief Returns the number of rows affected by the executed SQL statement. * \details + * Note that this is NOT the number of matched rows. If a row + * is matched but not affected, it won't be accounted for here. + * * For operations returning more than one resultset, returns the * first resultset's affected rows. * diff --git a/include/boost/mysql/static_execution_state.hpp b/include/boost/mysql/static_execution_state.hpp index 05ab3fc48..26541de00 100644 --- a/include/boost/mysql/static_execution_state.hpp +++ b/include/boost/mysql/static_execution_state.hpp @@ -160,6 +160,9 @@ class static_execution_state /** * \brief Returns the number of rows affected by the SQL statement associated to this resultset. + * Note that this is NOT the number of matched rows. If a row + * is matched but not affected, it won't be accounted for here. + * * \par Exception safety * No-throw guarantee. * diff --git a/include/boost/mysql/static_results.hpp b/include/boost/mysql/static_results.hpp index cc5cba513..69f8bc1af 100644 --- a/include/boost/mysql/static_results.hpp +++ b/include/boost/mysql/static_results.hpp @@ -179,6 +179,9 @@ class static_results /** * \brief Returns the number of rows affected by the executed SQL statement. * \details + * Note that this is NOT the number of matched rows. If a row + * is matched but not affected, it won't be accounted for here. + * * \tparam I Resultset index. For operations returning more than one resultset, you can explicitly * specify this parameter to obtain the number of affected rows by the i-th resultset. If left * unspecified, the number of affected rows by the first resultset is returned. diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index d1f2b2695..f8fcfbbcc 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -32,7 +32,9 @@ add_executable( test/database_types.cpp # Snippets + test/snippets/tutorials.cpp test/snippets/overview.cpp + test/snippets/connection_establishment.cpp test/snippets/dynamic.cpp test/snippets/static.cpp test/snippets/prepared_statements.cpp @@ -41,7 +43,6 @@ add_executable( test/snippets/metadata.cpp test/snippets/charsets.cpp test/snippets/time_types.cpp - test/snippets/any_connection.cpp test/snippets/connection_pool.cpp test/snippets/sql_formatting.cpp test/snippets/sql_formatting_custom.cpp @@ -56,7 +57,6 @@ target_link_libraries( boost_mysql_integrationtests PRIVATE boost_mysql_testing - Boost::context ) boost_mysql_common_target_settings(boost_mysql_integrationtests) if (${CMAKE_VERSION} VERSION_GREATER_EQUAL 3.16) diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 341da1dd9..30390bdf3 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -9,7 +9,6 @@ cpp-pch pch : pch.hpp /boost/mysql/test//boost_mysql_test - /boost/mysql/test//boost_context_lib ; @@ -17,7 +16,6 @@ run pch /boost/mysql/test//common_test_sources /boost/mysql/test//boost_mysql_test - /boost/mysql/test//boost_context_lib /boost/mysql/test//launch_with_valgrind # Utilities @@ -44,7 +42,9 @@ run test/database_types.cpp # Snippets + test/snippets/tutorials.cpp test/snippets/overview.cpp + test/snippets/connection_establishment.cpp test/snippets/dynamic.cpp test/snippets/static.cpp test/snippets/prepared_statements.cpp @@ -53,7 +53,6 @@ run test/snippets/metadata.cpp test/snippets/charsets.cpp test/snippets/time_types.cpp - test/snippets/any_connection.cpp test/snippets/connection_pool.cpp test/snippets/sql_formatting.cpp test/snippets/sql_formatting_custom.cpp diff --git a/test/integration/include/test_integration/snippets/snippets_fixture.hpp b/test/integration/include/test_integration/snippets/snippets_fixture.hpp new file mode 100644 index 000000000..43e9787d2 --- /dev/null +++ b/test/integration/include/test_integration/snippets/snippets_fixture.hpp @@ -0,0 +1,42 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_SNIPPETS_FIXTURE_HPP +#define BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_SNIPPETS_FIXTURE_HPP + +#include + +#include "test_common/ci_server.hpp" +#include "test_integration/any_connection_fixture.hpp" +#include "test_integration/snippets/credentials.hpp" + +namespace boost { +namespace mysql { +namespace test { + +inline connect_params snippets_connect_params() +{ + connect_params params; + params.server_address.emplace_host_and_port(get_hostname()); + params.username = mysql_username; + params.password = mysql_password; + params.database = "boost_mysql_examples"; + params.ssl = ssl_mode::disable; + params.multi_queries = true; + return params; +} + +struct snippets_fixture : any_connection_fixture +{ + snippets_fixture() { connect(snippets_connect_params()); } +}; + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/integration/test/snippets/any_connection.cpp b/test/integration/test/snippets/any_connection.cpp deleted file mode 100644 index a0c8bdf54..000000000 --- a/test/integration/test/snippets/any_connection.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// -// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include "test_common/ci_server.hpp" -#include "test_integration/server_features.hpp" -#include "test_integration/snippets/credentials.hpp" - -using namespace boost::mysql; -using namespace boost::mysql::test; - -namespace { - -//[any_connection_tcp -void create_and_connect( - string_view server_hostname, - string_view username, - string_view password, - string_view database -) -{ - // connect_params contains all the info required to establish a session - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(server_hostname); // server host - params.username = username; // username to log in as - params.password = password; // password to use - params.database = database; // database to use - - // The execution context, required to run I/O operations. - boost::asio::io_context ctx; - - // A connection to the server. Note how the type doesn't depend - // on the transport being used. - boost::mysql::any_connection conn(ctx); - - // Connect to the server. This will perform hostname resolution, - // TCP-level connect, and the MySQL handshake. After this function - // succeeds, your connection is ready to run queries - conn.connect(params); -} -//] - -BOOST_AUTO_TEST_CASE(section_any_connection_tcp) -{ - create_and_connect(get_hostname(), mysql_username, mysql_password, "boost_mysql_examples"); -} - -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - -//[any_connection_unix -void create_and_connect_unix(string_view username, string_view password, string_view database) -{ - // server_address may contain a UNIX socket path, too - boost::mysql::connect_params params; - params.server_address.emplace_unix_path("/var/run/mysqld/mysqld.sock"); - params.username = username; // username to log in as - params.password = password; // password to use - params.database = database; // database to use - - // The execution context, required to run I/O operations. - boost::asio::io_context ctx; - - // A connection to the server. Note how the type doesn't depend - // on the transport being used. - boost::mysql::any_connection conn(ctx); - - // Connect to the server. This will perform the - // UNIX socket connect and the MySQL handshake. After this function - // succeeds, your connection is ready to run queries - conn.connect(params); -} -//] - -BOOST_TEST_DECORATOR(*run_if(&server_features::unix_sockets)) -BOOST_AUTO_TEST_CASE(section_any_connection_unix) -{ - create_and_connect_unix(mysql_username, mysql_password, "boost_mysql_examples"); -} -#endif - -//[any_connection_reconnect -error_code connect_with_retries( - boost::mysql::any_connection& conn, - const boost::mysql::connect_params& params -) -{ - // We will be using the non-throwing overloads - error_code ec; - diagnostics diag; - - // Try to connect at most 10 times - for (int i = 0; i < 10; ++i) - { - // Try to connect - conn.connect(params, ec, diag); - - // If we succeeded, we're done - if (!ec) - return error_code(); - - // Whoops, connect failed. We can sleep and try again - std::cerr << "Failed connecting to MySQL: " << ec << ": " << diag.server_message() << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - - // No luck, retries expired - return ec; -} -//] - -BOOST_AUTO_TEST_CASE(section_any_connection) -{ - auto server_hostname = get_hostname(); - - { - connect_params params; - params.server_address.emplace_host_and_port(server_hostname); - params.username = mysql_username; - params.password = mysql_password; - - boost::asio::io_context ctx; - any_connection conn(ctx); - auto ec = connect_with_retries(conn, params); - BOOST_TEST(ec == error_code()); - } - - { - connect_params params; - - //[any_connection_ssl_mode - // Don't ever use TLS, even if the server supports it - params.ssl = boost::mysql::ssl_mode::disable; - - // ... - - // Force using TLS. If the server doesn't support it, reject the connection - params.ssl = boost::mysql::ssl_mode::require; - //] - } - - { - //[any_connection_ssl_ctx - // The I/O context required to run network operations - boost::asio::io_context ctx; - - // Create a SSL context - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tlsv12_client); - - // Set options on the SSL context. Load the default certificate authorities - // and enable certificate verification. connect will fail if the server certificate - // isn't signed by a trusted entity or its hostname isn't "mysql" - ssl_ctx.set_default_verify_paths(); - ssl_ctx.set_verify_mode(boost::asio::ssl::verify_peer); - ssl_ctx.set_verify_callback(boost::asio::ssl::host_name_verification("mysql")); - - // Construct an any_connection object passing the SSL context. - // You must keep ssl_ctx alive while using the connection. - boost::mysql::any_connection_params ctor_params; - ctor_params.ssl_context = &ssl_ctx; - boost::mysql::any_connection conn(ctx, ctor_params); - - // Connect params - boost::mysql::connect_params params; - params.server_address.emplace_host_and_port(server_hostname); // server host - params.username = mysql_username; // username to log in as - params.password = mysql_password; // password to use - params.ssl = boost::mysql::ssl_mode::require; // fail if TLS is not available - - // Connect - error_code ec; - diagnostics diag; - conn.connect(params, ec, diag); - if (ec) - { - // Handle error - } - //] - BOOST_TEST(ec != error_code()); - BOOST_TEST((ec.category() == boost::asio::error::get_ssl_category())); - } -} - -} // namespace diff --git a/test/integration/test/snippets/charsets.cpp b/test/integration/test/snippets/charsets.cpp index 95e270db3..a00f85c36 100644 --- a/test/integration/test/snippets/charsets.cpp +++ b/test/integration/test/snippets/charsets.cpp @@ -5,17 +5,14 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include + #include #include #include #include -#include "test_integration/snippets/get_connection.hpp" - -using namespace boost::mysql; -using namespace boost::mysql::test; - namespace { //[charsets_next_char @@ -68,15 +65,6 @@ std::size_t utf8mb4_next_char(boost::span input) BOOST_AUTO_TEST_CASE(section_charsets) { - auto& conn = get_connection(); - - { - //[charsets_set_names - results result; - conn.execute("SET NAMES utf8mb4", result); - // Further operations can assume utf8mb4 as conn's charset - //] - } { // Verify that utf8mb4_next_char can be used in a character_set boost::mysql::character_set charset{"utf8mb4", utf8mb4_next_char}; diff --git a/test/integration/test/snippets/connection_establishment.cpp b/test/integration/test/snippets/connection_establishment.cpp new file mode 100644 index 000000000..4a7f3a4e6 --- /dev/null +++ b/test/integration/test/snippets/connection_establishment.cpp @@ -0,0 +1,58 @@ +// +// 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 "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; +using namespace boost::mysql::test; + +namespace { + +BOOST_FIXTURE_TEST_CASE(section_connection_establishment, io_context_fixture) +{ + { + //[section_connection_establishment_max_buffer_size + // Increase the max buffer size to 512MB. + // This allows reading individual rows as big as 512MB. + // This is only required if each individual row is extremely big, + // and is not required for many smaller rows. + mysql::any_connection_params conn_params; + conn_params.max_buffer_size = 0x20000000; + + // Create the connection + mysql::any_connection conn(ctx, conn_params); + + // 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/overview.cpp b/test/integration/test/snippets/overview.cpp index 1f20e1572..310353bd8 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -5,144 +5,142 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT + +#include +#include +#include +#include #include #include #include #include -#include -#include +#include -#include #include +#include +#include +#include #include -#include -#include -#include -#include -#include +#include +#include #include +#include #include #include "test_common/ci_server.hpp" +#include "test_integration/any_connection_fixture.hpp" #include "test_integration/run_coro.hpp" #include "test_integration/snippets/credentials.hpp" -#include "test_integration/snippets/describe.hpp" -#include "test_integration/snippets/get_any_connection.hpp" -#include "test_integration/snippets/get_connection.hpp" - -#ifdef BOOST_ASIO_HAS_CO_AWAIT -#include -#endif -using namespace boost::mysql; +namespace mysql = boost::mysql; +namespace asio = boost::asio; using namespace boost::mysql::test; // Defined outside the namespace to prevent unused warnings -#if defined(BOOST_ASIO_HAS_CO_AWAIT) && !defined(BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT) -boost::asio::awaitable dont_run() +#if !defined(BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT) +asio::awaitable dont_run(mysql::any_connection& conn) { - using namespace boost::asio::experimental::awaitable_operators; - - // Setup - auto& conn = get_connection(); - //[overview_async_dont // Coroutine body // DO NOT DO THIS!!!! - results result1, result2; - co_await ( - conn.async_execute("SELECT 1", result1, boost::asio::use_awaitable) && - conn.async_execute("SELECT 2", result2, boost::asio::use_awaitable) - ); + mysql::results result1, result2; + co_await asio::experimental::make_parallel_group( + conn.async_execute("SELECT 1", result1, asio::deferred), + conn.async_execute("SELECT 2", result2, asio::deferred) + ) + .async_wait(asio::experimental::wait_for_all(), asio::deferred); //] } #endif -namespace { - -const char* get_value_from_user() { return ""; } - -BOOST_AUTO_TEST_CASE(section_overview) +inline namespace overview { +//[overview_static_struct +// This must be placed at namespace scope. +// Should contain a member for each field of interest present in our query. +// Declaration order doesn't need to match field order in the query. +// Field names should match the ones in our query +struct employee { - //[overview_connection - // The execution context, required to run I/O operations. - boost::asio::io_context ctx; - - // The SSL context, required to establish TLS connections. - // The default SSL options are good enough for us at this point. - boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tls_client); + std::int64_t id; + std::string first_name; + std::string last_name; +}; +//] +} // namespace overview - // Represents a connection to the MySQL server. - boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); - //] +namespace { - //[overview_connect - // Obtain the hostname to connect to - replace get_hostname by your code +asio::awaitable overview_coro(mysql::any_connection& conn) +{ std::string server_hostname = get_hostname(); + int employee_id = 1; + const char* new_name = "John"; - // Resolve the hostname to get a collection of endpoints - boost::asio::ip::tcp::resolver resolver(ctx.get_executor()); - auto endpoints = resolver.resolve(server_hostname, boost::mysql::default_port_string); - - // The username and password to use - boost::mysql::handshake_params params( - mysql_username, // username, as a string - mysql_password, // password, as a string - don't hardcode this into your code! - "boost_mysql_examples" // database to use - ); - - // Connect to the server using the first endpoint returned by the resolver - conn.connect(*endpoints.begin(), params); - //] + auto& ctx = static_cast((co_await asio::this_coro::executor).context()); { - //[overview_query_use_case - results result; - conn.execute("START TRANSACTION", result); + //[overview_connect + // The hostname, username, password and database to use. + mysql::connect_params params; + params.server_address.emplace_host_and_port(server_hostname); // hostname + params.username = mysql_username; + params.password = mysql_password; + params.database = "boost_mysql_examples"; + + // Connect to the server + co_await conn.async_connect(params); //] } { - //[overview_statement_use_case - statement stmt = conn.prepare_statement( - "SELECT first_name FROM employee WHERE company_id = ? AND salary > ?" - ); - - results result; - conn.execute(stmt.bind("HGS", 30000), result); + //[overview_text_query + // Executes 'SELECT 1' and reads the resulting rows into memory + mysql::results result; + co_await conn.async_execute("SELECT 1", result); //] } + { - //[overview_ifaces_table - const char* table_definition = R"%( - CREATE TEMPORARY TABLE posts ( - id INT PRIMARY KEY AUTO_INCREMENT, - title VARCHAR (256) NOT NULL, - body TEXT NOT NULL - ) - )%"; + //[overview_with_params + // If employee_id is 42, executes 'SELECT first_name FROM employee WHERE id = 42' + mysql::results result; + co_await conn.async_execute( + mysql::with_params("SELECT first_name FROM employee WHERE id = {}", employee_id), + result + ); //] + } + + { + //[overview_statement + // First prepare the statement. Parsing happens server-side. + mysql::statement stmt = co_await conn.async_prepare_statement( + "SELECT first_name FROM employee WHERE company_id = ?" + ); - results result; - conn.execute(table_definition, result); + // Now execute it. Parameter substitution happens server-side. + mysql::results result; + co_await conn.async_execute(stmt.bind(employee_id), result); + //] } { //[overview_ifaces_dynamic - // Passing a results object to connection::execute selects the dynamic interface - results result; - conn.execute("SELECT id, title, body FROM posts", result); + // Passing a results to async_execute selects the dynamic interface + mysql::results result; + co_await conn.async_execute("SELECT id, first_name, last_name FROM employee", result); - // Every row is a collection of fields, which are variant-like objects + // Every employee is a collection of fields, which are variant-like objects // that represent data. We use as_string() to cast them to the appropriate type - for (row_view post : result.rows()) + for (mysql::row_view emp : result.rows()) { - std::cout << "Title: " << post.at(1).as_string() << "Body: " << post.at(2).as_string() + std::cout << "First name: " << emp.at(1).as_string() << ", last name: " << emp.at(2).as_string() << std::endl; } //] } -#ifdef BOOST_MYSQL_CXX14 +#if BOOST_PFR_CORE_NAME_ENABLED { // The struct definition is included above this //[overview_ifaces_static @@ -150,62 +148,36 @@ BOOST_AUTO_TEST_CASE(section_overview) // This must be placed inside your function or method: // - // Passing a static_results to execute() selects the static interface - static_results result; - conn.execute("SELECT id, title, body FROM posts", result); + // Passing a static_results to async_execute selects the static interface + mysql::static_results> result; + co_await conn.async_execute("SELECT id, first_name, last_name FROM employee", result); // Query results are parsed directly into your own type - for (const post& p : result.rows()) + for (const employee& emp : result.rows()) { - std::cout << "Title: " << p.title << "Body: " << p.body << std::endl; + std::cout << "First name: " << emp.first_name << ", last name: " << emp.last_name << std::endl; } //] } #endif - { - //[overview_statements_setup - results result; - conn.execute( - R"%( - CREATE TEMPORARY TABLE products ( - id VARCHAR(50) PRIMARY KEY, - description VARCHAR(256) - ) - )%", + //[overview_update + mysql::results result; + co_await conn.async_execute( + mysql::with_params("UPDATE employee SET first_name = {} WHERE id = {}", new_name, employee_id), result ); - conn.execute("INSERT INTO products VALUES ('PTT', 'Potatoes'), ('CAR', 'Carrots')", result); //] } { - //[overview_statements_prepare - statement stmt = conn.prepare_statement("SELECT description FROM products WHERE id = ?"); - //] - - //[overview_statements_execute - // Obtain the product_id from the user. product_id is untrusted input - const char* product_id = get_value_from_user(); - - // Execute the statement - results result; - conn.execute(stmt.bind(product_id), result); - - // Use result as required - //] - - conn.execute("DROP TABLE products", result); - } - { - //[overview_errors_sync_errc - error_code ec; - diagnostics diag; - results result; + //[overview_no_exceptions + mysql::error_code ec; + mysql::diagnostics diag; + mysql::results result; // The provided SQL is invalid. The server will return an error. - // ec will be set to a non-zero value - conn.execute("this is not SQL!", result, ec, diag); - + // ec will be set to a non-zero value, and diag will be populated + co_await conn.async_execute("this is not SQL!", result, diag, asio::redirect_error(ec)); if (ec) { // The error code will likely report a syntax error @@ -217,126 +189,65 @@ BOOST_AUTO_TEST_CASE(section_overview) std::cout << "Server diagnostics: " << diag.server_message() << std::endl; } //] - } - { - //[overview_errors_sync_exc - try - { - // The provided SQL is invalid. This function will throw an exception. - results result; - conn.execute("this is not SQL!", result); - } - catch (const error_with_diagnostics& err) - { - // error_with_diagnostics contains an error_code and a diagnostics object. - // It inherits from boost::system::system_error. - std::cout << "Operation failed with error code: " << err.code() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - } - //] - } - { -#ifdef BOOST_ASIO_HAS_CO_AWAIT - run_coro(ctx, [&conn]() -> boost::asio::awaitable { - //[overview_async_coroutinescpp20 - // Using this CompletionToken, you get C++20 coroutines that communicate - // errors with error_codes. This way, you can access the diagnostics object. - constexpr auto token = boost::asio::as_tuple(boost::asio::use_awaitable); - - // Run our query as a coroutine - diagnostics diag; - results result; - auto [ec] = co_await conn.async_execute("SELECT 'Hello world!'", result, diag, token); - - // This will throw an error_with_diagnostics in case of failure - boost::mysql::throw_on_error(ec, diag); - //] - }); -#endif - } - { - results r; - conn.execute("DROP TABLE IF EXISTS posts", r); + BOOST_TEST(ec != mysql::error_code()); } { //[overview_multifn - // Create the table and some sample data - // In a real system, body may be megabytes long. - results result; - conn.execute( - R"%( - CREATE TEMPORARY TABLE posts ( - id INT PRIMARY KEY AUTO_INCREMENT, - title VARCHAR (256), - body TEXT - ) - )%", - result - ); - conn.execute( - R"%( - INSERT INTO posts (title, body) VALUES - ('Post 1', 'A very long post body'), - ('Post 2', 'An even longer post body') - )%", - result - ); - // execution_state stores state about our operation, and must be passed to all functions - execution_state st; + mysql::execution_state st; // Writes the query request and reads the server response, but not the rows - conn.start_execution("SELECT title, body FROM posts", st); + co_await conn.async_start_execution("SELECT first_name, last_name FROM employee", st); // Reads all the returned rows, in batches. - // st.complete() returns true once there are no more rows to read - while (!st.complete()) + // st.should_read_rows() returns false once there are no more rows to read + while (st.should_read_rows()) { // row_batch will be valid until conn performs the next network operation - rows_view row_batch = conn.read_some_rows(st); + mysql::rows_view row_batch = co_await conn.async_read_some_rows(st); - for (row_view post : row_batch) + for (mysql::row_view emp : row_batch) { - // Process post as required - std::cout << "Title:" << post.at(0) << std::endl; + // Process the employee as required + std::cout << "Name:" << emp.at(0) << " " << emp.at(1) << std::endl; } } //] - - conn.execute("DROP TABLE posts", result); } -} - -// The async section is small enough to just be here -BOOST_AUTO_TEST_CASE(section_async) -{ -#ifdef BOOST_ASIO_HAS_CO_AWAIT - auto& conn = get_any_connection(); - auto& ctx = static_cast(conn.get_executor().context()); - results result; - - run_coro(ctx, [&]() -> boost::asio::awaitable { - //[async_with_diagnostics_cpp20 - // C++20. Will throw error_with_diagnostics on error - co_await conn.async_execute("SELECT 1", result, with_diagnostics(boost::asio::deferred)); - - // If you're using any_connection, with_diagnostics(asio::deferred) is the default token, - // so you can just write: - co_await conn.async_execute("SELECT 1", result); + { + //[overview_pool_create + // 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 = mysql_username; + params.password = mysql_password; + params.database = "boost_mysql_examples"; + + // Construct a pool of connections. The execution context will be used internally + // 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); //] - }); -#endif + + // If we don't use the pool, we may leave unfinished work in the context + co_await pool.async_get_connection(); + } } -BOOST_ATTRIBUTE_UNUSED -void section_async_cpp11(boost::asio::yield_context yield) +BOOST_FIXTURE_TEST_CASE(section_overview, any_connection_fixture) { - auto& conn = get_connection(); - results result; - //[async_with_diagnostics_cpp11 - // C++11. Will throw error_with_diagnostics on error - conn.async_execute("SELECT 1", result, with_diagnostics(yield)); - //] + run_coro(ctx, [&]() { return overview_coro(conn); }); } } // namespace + +#endif \ No newline at end of file diff --git a/test/integration/test/snippets/sql_formatting.cpp b/test/integration/test/snippets/sql_formatting.cpp index 4c2b2b385..26ccd7769 100644 --- a/test/integration/test/snippets/sql_formatting.cpp +++ b/test/integration/test/snippets/sql_formatting.cpp @@ -84,6 +84,7 @@ std::string compose_select_query( } // namespace +inline namespace sql_formatting { //[sql_formatting_formatter_specialization // We want to add formatting support for employee struct employee @@ -92,6 +93,9 @@ struct employee std::string last_name; std::string company_id; }; +//<- +} // namespace sql_formatting +//-> namespace boost { namespace mysql { diff --git a/test/integration/test/snippets/tutorials.cpp b/test/integration/test/snippets/tutorials.cpp new file mode 100644 index 000000000..7c652b01e --- /dev/null +++ b/test/integration/test/snippets/tutorials.cpp @@ -0,0 +1,39 @@ +// +// 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 "test_integration/snippets/snippets_fixture.hpp" + +namespace mysql = boost::mysql; +using namespace mysql::test; + +namespace { + +// Taken here because it's only used in the discussion +void print_employee(mysql::string_view first_name, mysql::string_view last_name) +{ + std::cout << "Employee's name is: " << first_name << ' ' << last_name << std::endl; +} + +BOOST_FIXTURE_TEST_CASE(section_tutorials, snippets_fixture) +{ + { + mysql::results result; + conn.execute("SELECT first_name, last_name FROM employee WHERE id = 1", result); + + //[tutorial_static_casts + mysql::row_view employee = result.rows().at(0); + print_employee(employee.at(0).as_string(), employee.at(1).as_string()); + //] + } +} + +} // namespace diff --git a/tools/ci/ci_util/db_setup.py b/tools/ci/ci_util/db_setup.py index 849a66513..70457bf18 100644 --- a/tools/ci/ci_util/db_setup.py +++ b/tools/ci/ci_util/db_setup.py @@ -92,7 +92,6 @@ def db_setup( # Source files _run_sql_file(source_dir.joinpath('example', 'db_setup.sql')) - _run_sql_file(source_dir.joinpath('example', 'order_management', 'db_setup.sql')) _run_sql_file(source_dir.joinpath('test', 'integration', 'db_setup.sql')) if not disabled_features['sha256']: _run_sql_file(source_dir.joinpath('test', 'integration', 'db_setup_sha256.sql')) diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py new file mode 100644 index 000000000..470a6cc74 --- /dev/null +++ b/tools/scripts/examples_qbk.py @@ -0,0 +1,215 @@ +#!/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) +# + +from typing import NamedTuple, List +from os import path, listdir + +REPO_BASE = path.abspath(path.join(path.dirname(path.realpath(__file__)), '..', '..')) + +class Example(NamedTuple): + id: str + path: str + title: str + + +class MultiExample(NamedTuple): + id: str + paths: List[str] + title: str + + +LINK_TEMPLATE = '* [link mysql.examples.{example.id} {example.title}]' +SECTION_TEMPLATE = ''' +[section:{example.id} {example.title}] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +{example_cpps} + +[endsect] +''' + +TEMPLATE='''[/ + 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) +] + +[/ This file was auto-generated by examples_qbk.py. Do not edit directly ] + +[section:examples Examples] + +To run the examples, please go through the [link mysql.examples.setup setup] first. + +Here is a list of available examples: + +[heading Tutorials] + +Self-contained programs demonstrating the basic concepts. + +{tutorial_links} + +[heading Simple programs] + +Self-contained programs demonstrating more advanced concepts and techniques. + +{simple_links} + +[heading Advanced examples] + +Programs implementing real-world functionality. + +{advanced_links} +# [@https://github.com/anarthal/servertech-chat The BoostServerTech chat project uses Boost.MySQL and Boost.Redis to implement a chat server] + +[heading Setup] + +To run the examples, you need a MySQL server you can connect to. +Examples make use of a database named `boost_mysql_examples`. +The server hostname and credentials (username and password) are passed +to the examples via the command line. + +You can spin up a server quickly by using Docker: + +[!teletype] +``` + # Remove the "-v /var/run/mysqld:/var/run/mysqld" part if you don't need UNIX sockets + > docker run --name some-mysql -p 3306:3306 -v /var/run/mysqld:/var/run/mysqld -d -e MYSQL_ROOT_PASSWORD= -e MYSQL_ALLOW_EMPTY_PASSWORD=1 -d mysql + + # All the required data can be loaded by running example/db_setup.sql. + # If you're using the above container, the root user has a blank password + > mysql -u root < example/db_setup.sql +``` + +Please note that this container is just for demonstrative purposes, +and is not suitable for production. + +The root MySQL user for these containers is `root` and has an empty password. + +{all_examples} + +[endsect] + +''' + +# 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'), +] + +SIMPLE_EXAMPLES = [ + 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'), + Example('multi_function', '2_simple/multi_function.cpp', 'Reading rows in batches with multi-function operations'), + Example('callbacks', '2_simple/callbacks.cpp', 'Callbacks (async functions in C++11)'), + Example('coroutines_cpp11', '2_simple/coroutines_cpp11.cpp', 'Stackful coroutines (async functions in C++11)'), + Example('unix_socket', '2_simple/unix_socket.cpp', 'UNIX sockets'), + Example('batch_inserts', '2_simple/batch_inserts.cpp', 'Batch inserts using client-side query formatting'), + Example('batch_inserts_generic', '2_simple/batch_inserts_generic.cpp', 'Generic batch inserts with Boost.Describe'), + Example('dynamic_filters', '2_simple/dynamic_filters.cpp', 'Queries with dynamic filters'), + Example('patch_updates', '2_simple/patch_updates.cpp', 'Dynamic UPDATE queries with PATCH-like semantics'), + Example('source_script', '2_simple/source_script.cpp', 'Sourcing a .sql file using multi-queries'), + Example('pipeline', '2_simple/pipeline.cpp', '(Experimental) Pipelines'), +] + +ADVANCED_EXAMPLES = [ + MultiExample('connection_pool', [ + '3_advanced/connection_pool/main.cpp', + '3_advanced/connection_pool/types.hpp', + '3_advanced/connection_pool/repository.hpp', + '3_advanced/connection_pool/repository.cpp', + '3_advanced/connection_pool/handle_request.hpp', + '3_advanced/connection_pool/handle_request.cpp', + '3_advanced/connection_pool/server.hpp', + '3_advanced/connection_pool/server.cpp', + '3_advanced/connection_pool/log_error.hpp', + ], 'A REST API server that uses connection pooling') +] + +ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES + +def _render_links(examples: List[Example]) -> str: + return '\n'.join(LINK_TEMPLATE.format(example=elm) for elm in examples) + +def _write_file(relpath: List[str], contents: str) -> None: + output_file = path.join(REPO_BASE, *relpath) + with open(output_file, 'wt') as f: + f.write(contents) + +def _render_simple_cpp(id: str) -> str: + return f'[example_{id}]' + +def _render_multi_cpp(id: str, paths: List[str]) -> str: + def get_file_id(p: str): + # File IDs follow the below convention + converted_id = path.basename(p).replace('.', '_') + return f'{id}_{converted_id}' + + return '\n\n'.join(_render_simple_cpp(get_file_id(p)) for p in paths) + +def _collect_snippets() -> List[str]: + snippets_relpath = ['test', 'integration', 'test', 'snippets'] + return [ + path.join(*snippets_relpath, p) + for p in listdir(path.join(REPO_BASE, *snippets_relpath)) + ] + +def _replace_imports(import_contents: str) -> None: + # Read the file + with open(path.join(REPO_BASE, 'doc', 'qbk', '00_main.qbk'), 'rt') as f: + contents = f.read() + + # Replace + begin_marker = '[/ AUTOGENERATED IMPORTS BEGIN ]\n' + end_marker = '\n[/ AUTOGENERATED IMPORTS END ]' + begin_pos = contents.find(begin_marker) + end_pos = contents.find(end_marker) + assert begin_pos != -1 + assert end_pos != -1 + final_contents = contents[:begin_pos + len(begin_marker)] + import_contents + contents[end_pos:] + + # Write the file + _write_file(['doc', 'qbk', '00_main.qbk'], final_contents) + + +def main(): + # Collect all files to be imported + example_paths = [e.path for e in ALL_EXAMPLES if isinstance(e, Example)] + for p in [e.paths for e in ALL_EXAMPLES if isinstance(e, MultiExample)]: + example_paths += p + all_paths = [f'example/{p}' for p in example_paths] + all_paths += _collect_snippets() + + + # Render + import_contents = '\n'.join(f'[import ../../{p}]' for p in all_paths) + example_contents = TEMPLATE.format( + tutorial_links=_render_links(TUTORIALS), + simple_links=_render_links(SIMPLE_EXAMPLES), + advanced_links='', + all_examples='\n\n\n'.join(SECTION_TEMPLATE.format( + example=elm, + example_cpps=_render_multi_cpp(elm.id, elm.paths) if isinstance(elm, MultiExample) else _render_simple_cpp(elm.id) + ) for elm in ALL_EXAMPLES) + ) + + # Write to output file + _replace_imports(import_contents) + _write_file(['doc', 'qbk', '21_examples.qbk'], example_contents) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/scripts/file_headers.py b/tools/scripts/file_headers.py index e86f2b9d7..d653de4cf 100755 --- a/tools/scripts/file_headers.py +++ b/tools/scripts/file_headers.py @@ -12,6 +12,12 @@ from typing import List, Tuple import glob from abc import abstractmethod, ABCMeta +import sys + +THIS_FOLDER = path.abspath(path.dirname(path.realpath(__file__))) +sys.path.append(path.join(THIS_FOLDER)) + +import examples_qbk # Script to get file headers (copyright notices # and include guards) okay and up to date @@ -284,6 +290,7 @@ def verify_test_consistency(): def main(): process_all_files() verify_test_consistency() + examples_qbk.main() if __name__ == '__main__':