From e803582100f1607f45fb5c02048f992c486cec27 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 20:43:00 +0200 Subject: [PATCH 01/77] Tutorials --- example/tutorial.cpp | 36 ++++------ example/tutorial_async.cpp | 139 +++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 example/tutorial_async.cpp diff --git a/example/tutorial.cpp b/example/tutorial.cpp index f0f2f5bb0..7b3852352 100644 --- a/example/tutorial.cpp +++ b/example/tutorial.cpp @@ -7,18 +7,14 @@ //[tutorial_listing +#include +#include #include -#include #include -#include #include -#include -#include -#include #include -#include /** * For this example, we will be using the 'boost_mysql_examples' database. @@ -40,28 +36,20 @@ void main_impl(int argc, char** argv) // 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); - // Represents a connection to the MySQL server. - boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); + boost::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 - ); - - // Connect to the server using the first endpoint returned by the resolver - conn.connect(*endpoints.begin(), params); + // The hostname, username, password and database to use + 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); //] //[tutorial_query diff --git a/example/tutorial_async.cpp b/example/tutorial_async.cpp new file mode 100644 index 000000000..99bf381db --- /dev/null +++ b/example/tutorial_async.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) +// + +// TODO: link this + +#include + +//<- +#ifdef BOOST_ASIO_HAS_CO_AWAIT +//-> + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +/** + * This example is analogous to the synchronous tutorial, but uses async functions + * with C++20 coroutines, instead. It uses the 'boost_mysql_examples' database. + * You can get this database by running db_setup.sql. + * + * This function implements the main coroutine. + * It must have a return type of boost::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. + */ +boost::asio::awaitable coro_main( + std::string_view server_hostname, + std::string_view username, + std::string_view password +) +{ + // Represents a connection to the MySQL server. + boost::mysql::any_connection conn(co_await boost::asio::this_coro::executor); + + // The hostname, username, password and database to use + boost::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 SQL query to the server + const char* sql = "SELECT 'Hello world!'"; + boost::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); + } + + // The execution context, required to run I/O operations. + boost::asio::io_context ctx; + + // The entry point. We pass in a function returning + // boost::asio::awaitable, as required. + // Calling co_spawn enqueues the coroutine for execution. + boost::asio::co_spawn( + ctx, + [argv] { 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(); +} + +//<- +#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; + } +} + +//] From 3559cd6fe6b9a4fae0c99acf94212903aec91a4f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 21:00:07 +0200 Subject: [PATCH 02/77] tutorial updates --- example/tutorial.cpp | 6 ++---- example/tutorial_async.cpp | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/example/tutorial.cpp b/example/tutorial.cpp index 7b3852352..e7072a690 100644 --- a/example/tutorial.cpp +++ b/example/tutorial.cpp @@ -17,10 +17,8 @@ #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 the 'boost_mysql_examples' database, which you + * can get by running db_setup.sql. * This example uses synchronous functions and handles errors using exceptions. */ diff --git a/example/tutorial_async.cpp b/example/tutorial_async.cpp index 99bf381db..54724cd17 100644 --- a/example/tutorial_async.cpp +++ b/example/tutorial_async.cpp @@ -13,6 +13,7 @@ #ifdef BOOST_ASIO_HAS_CO_AWAIT //-> +#include #include #include #include @@ -24,7 +25,6 @@ #include #include -#include /** * This example is analogous to the synchronous tutorial, but uses async functions @@ -42,20 +42,21 @@ * sync functions by their async equivalents and adding co_await in front of them. */ boost::asio::awaitable coro_main( - std::string_view server_hostname, - std::string_view username, - std::string_view password + std::string server_hostname, + std::string username, + std::string password ) { // Represents a connection to the MySQL server. boost::mysql::any_connection conn(co_await boost::asio::this_coro::executor); // The hostname, username, password and database to use - boost::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"; + boost::mysql::connect_params params{ + .server_address = boost::mysql::host_and_port{std::move(server_hostname)}, + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples" + }; // Connect to the server co_await conn.async_connect(params); From 53be729382869c1f48a94cc62d00517855215c7f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 21:20:04 +0200 Subject: [PATCH 03/77] Revamp the callbacks example --- example/async_callbacks.cpp | 167 -------------------------------- example/simple/callbacks.cpp | 178 +++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 167 deletions(-) delete mode 100644 example/async_callbacks.cpp create mode 100644 example/simple/callbacks.cpp 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/simple/callbacks.cpp b/example/simple/callbacks.cpp new file mode 100644 index 000000000..1b2196329 --- /dev/null +++ b/example/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_async_callbacks + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +/** + * 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. + */ + +// 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; + } +} + +//] From f24909180e5a79d9541b1d61b027d4c18bef22d1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 21:25:03 +0200 Subject: [PATCH 04/77] Namespace aliases for tutorials --- example/tutorial.cpp | 13 ++++++++----- example/tutorial_async.cpp | 23 +++++++++++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/example/tutorial.cpp b/example/tutorial.cpp index e7072a690..d4a43b9c9 100644 --- a/example/tutorial.cpp +++ b/example/tutorial.cpp @@ -16,6 +16,9 @@ #include +namespace mysql = boost::mysql; +namespace asio = boost::asio; + /** * This example uses the 'boost_mysql_examples' database, which you * can get by running db_setup.sql. @@ -32,15 +35,15 @@ void main_impl(int argc, char** argv) //[tutorial_connection // The execution context, required to run I/O operations. - boost::asio::io_context ctx; + asio::io_context ctx; // Represents a connection to the MySQL server. - boost::mysql::any_connection conn(ctx); + mysql::any_connection conn(ctx); //] //[tutorial_connect // The hostname, username, password and database to use - boost::mysql::connect_params params; + mysql::connect_params params; params.server_address.emplace_host_and_port(argv[3]); params.username = argv[1]; params.password = argv[2]; @@ -53,7 +56,7 @@ void main_impl(int argc, char** argv) //[tutorial_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); //] @@ -74,7 +77,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/tutorial_async.cpp b/example/tutorial_async.cpp index 54724cd17..b370a64e6 100644 --- a/example/tutorial_async.cpp +++ b/example/tutorial_async.cpp @@ -26,6 +26,9 @@ #include #include +namespace mysql = boost::mysql; +namespace asio = boost::asio; + /** * This example is analogous to the synchronous tutorial, but uses async functions * with C++20 coroutines, instead. It uses the 'boost_mysql_examples' database. @@ -41,18 +44,14 @@ * 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. */ -boost::asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password -) +asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) { // Represents a connection to the MySQL server. - boost::mysql::any_connection conn(co_await boost::asio::this_coro::executor); + mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - boost::mysql::connect_params params{ - .server_address = boost::mysql::host_and_port{std::move(server_hostname)}, + mysql::connect_params params{ + .server_address = mysql::host_and_port{std::move(server_hostname)}, .username = std::move(username), .password = std::move(password), .database = "boost_mysql_examples" @@ -63,7 +62,7 @@ boost::asio::awaitable coro_main( // Issue the SQL query to the server const char* sql = "SELECT 'Hello world!'"; - boost::mysql::results result; + mysql::results result; co_await conn.async_execute(sql, result); // Print the first field in the first row @@ -82,12 +81,12 @@ void main_impl(int argc, char** argv) } // The execution context, required to run I/O operations. - boost::asio::io_context ctx; + asio::io_context ctx; // The entry point. We pass in a function returning // boost::asio::awaitable, as required. // Calling co_spawn enqueues the coroutine for execution. - boost::asio::co_spawn( + asio::co_spawn( ctx, [argv] { return coro_main(argv[3], argv[1], argv[2]); }, // If any exception is thrown in the coroutine body, rethrow it. @@ -120,7 +119,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 From e4f059ef293fbfd897144f7afb8e5c28c882660a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 21:35:31 +0200 Subject: [PATCH 05/77] Revamp the coroutines c++11 example --- example/simple/coroutines_cpp11.cpp | 151 ++++++++++++++++++++++++++++ example/tutorial_async.cpp | 1 + 2 files changed, 152 insertions(+) create mode 100644 example/simple/coroutines_cpp11.cpp diff --git a/example/simple/coroutines_cpp11.cpp b/example/simple/coroutines_cpp11.cpp new file mode 100644 index 000000000..56fa8ba10 --- /dev/null +++ b/example/simple/coroutines_cpp11.cpp @@ -0,0 +1,151 @@ +// +// 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 + +/** + * 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 + */ + +#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, + 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/tutorial_async.cpp b/example/tutorial_async.cpp index b370a64e6..d96f75347 100644 --- a/example/tutorial_async.cpp +++ b/example/tutorial_async.cpp @@ -47,6 +47,7 @@ namespace asio = boost::asio; asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) { // 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 From 350ae8837220168ec1789943a97cd7dc266a0ae3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Sep 2024 21:36:01 +0200 Subject: [PATCH 06/77] Remove obsolete examples --- example/any_connection.cpp | 159 --------------------------- example/async_coroutines.cpp | 140 ------------------------ example/async_coroutinescpp20.cpp | 173 ------------------------------ example/async_futures.cpp | 160 --------------------------- 4 files changed, 632 deletions(-) delete mode 100644 example/any_connection.cpp delete mode 100644 example/async_coroutines.cpp delete mode 100644 example/async_coroutinescpp20.cpp delete mode 100644 example/async_futures.cpp diff --git a/example/any_connection.cpp b/example/any_connection.cpp deleted file mode 100644 index 4fe52fd5b..000000000 --- a/example/any_connection.cpp +++ /dev/null @@ -1,159 +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. -// -// any_connection is an experimental feature. - -#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_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; - } -} - -//] From 44e0faee0bfff906c62450e51c8cbf775299fcb0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 3 Oct 2024 18:50:09 +0200 Subject: [PATCH 07/77] batch inserts --- example/{ => simple}/batch_inserts.cpp | 153 +++++++++++++++---------- example/tutorial_async.cpp | 2 +- 2 files changed, 93 insertions(+), 62 deletions(-) rename example/{ => simple}/batch_inserts.cpp (68%) diff --git a/example/batch_inserts.cpp b/example/simple/batch_inserts.cpp similarity index 68% rename from example/batch_inserts.cpp rename to example/simple/batch_inserts.cpp index 6bef74212..051ae7d67 100644 --- a/example/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -5,37 +5,44 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +//<- +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT +//-> -#ifdef BOOST_DESCRIBE_CXX14 +/** + * This example demonstrates how to insert several records in a single + * SQL statement using format_sql. It uses C++20 coroutines. + * + * 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. + */ //[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. -// -// Note: client-side SQL formatting is an experimental feature. - +#include #include #include #include #include +#include +#include #include -#include -#include +#include +#include #include #include #include #include +#include #include +namespace asio = boost::asio; +namespace mysql = boost::mysql; + /** * We will use Boost.Describe to easily parse the JSON file * into a std::vector. The JSON file contain an array @@ -56,6 +63,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 @@ -67,58 +75,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) +asio::awaitable coro_main( + std::string server_hostname, + std::string username, + std::string password, + std::span 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]; - params.database = "boost_mysql_examples"; - - // A results object to hold the result of executing our SQL query - boost::mysql::results result; + // Create a connection. + // Will use the same executor as the coroutine. + boost::mysql::any_connection conn(co_await asio::this_coro::executor); + + // The hostname, username, password and database to use + boost::mysql::connect_params params{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples" + }; // 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, @@ -133,17 +120,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) - 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(values, format_employee_fn) + mysql::sequence(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 = 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 + 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) @@ -152,7 +183,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/tutorial_async.cpp b/example/tutorial_async.cpp index d96f75347..8335a5244 100644 --- a/example/tutorial_async.cpp +++ b/example/tutorial_async.cpp @@ -7,9 +7,9 @@ // TODO: link this +//<- #include -//<- #ifdef BOOST_ASIO_HAS_CO_AWAIT //-> From cc6b88785fbf9fb467f78e156d6f74c4a86e673a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 9 Oct 2024 08:49:12 +0200 Subject: [PATCH 08/77] batch_inserts_generic --- example/simple/batch_inserts.cpp | 3 +- .../{ => simple}/batch_inserts_generic.cpp | 136 +++++++++++------- 2 files changed, 86 insertions(+), 53 deletions(-) rename example/{ => simple}/batch_inserts_generic.cpp (67%) diff --git a/example/simple/batch_inserts.cpp b/example/simple/batch_inserts.cpp index 051ae7d67..2355a1e64 100644 --- a/example/simple/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -42,6 +42,7 @@ namespace asio = boost::asio; namespace mysql = boost::mysql; +namespace json = boost::json; /** * We will use Boost.Describe to easily parse the JSON file @@ -146,7 +147,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)); + auto values = json::value_to>(json::parse(contents)); // We need one employee, at least if (values.empty()) diff --git a/example/batch_inserts_generic.cpp b/example/simple/batch_inserts_generic.cpp similarity index 67% rename from example/batch_inserts_generic.cpp rename to example/simple/batch_inserts_generic.cpp index 4b911e7a7..13a5cae35 100644 --- a/example/batch_inserts_generic.cpp +++ b/example/simple/batch_inserts_generic.cpp @@ -5,29 +5,32 @@ // 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. -// -// Note: client-side SQL formatting is an experimental feature. +/** + * 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. + * It uses C++20 coroutines. + * + * 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. + */ #include #include #include #include -#include #include +#include +#include #include #include #include @@ -42,10 +45,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, @@ -74,11 +80,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>() ); @@ -91,13 +97,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...} }; }, @@ -105,7 +111,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); } }; @@ -118,6 +124,45 @@ 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 server_hostname, + std::string username, + std::string password, + std::span 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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .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(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) @@ -131,25 +176,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()) @@ -158,21 +185,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. - boost::mysql::results result; - conn.execute( - boost::mysql::with_params( - "INSERT INTO employee ({::i}) VALUES {}", - get_field_names(), - boost::mysql::sequence(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) @@ -181,7 +213,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 From 75a1026cdb24b9c307f483e6afc770675dc40beb Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 13:07:36 +0200 Subject: [PATCH 09/77] Patch updates example --- example/simple/batch_inserts.cpp | 10 +- example/simple/batch_inserts_generic.cpp | 5 +- example/simple/patch_updates.cpp | 280 +++++++++++++++++++++++ 3 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 example/simple/patch_updates.cpp diff --git a/example/simple/batch_inserts.cpp b/example/simple/batch_inserts.cpp index 2355a1e64..d028dded4 100644 --- a/example/simple/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -10,17 +10,21 @@ #ifdef BOOST_ASIO_HAS_CO_AWAIT //-> +//[example_batch_inserts + /** * This example demonstrates how to insert several records in a single - * SQL statement using format_sql. It uses C++20 coroutines. + * 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. */ -//[example_batch_inserts - #include #include #include diff --git a/example/simple/batch_inserts_generic.cpp b/example/simple/batch_inserts_generic.cpp index 13a5cae35..ea0f7cc5c 100644 --- a/example/simple/batch_inserts_generic.cpp +++ b/example/simple/batch_inserts_generic.cpp @@ -16,11 +16,14 @@ * 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. - * It uses C++20 coroutines. * * 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. */ #include diff --git a/example/simple/patch_updates.cpp b/example/simple/patch_updates.cpp new file mode 100644 index 000000000..251867a8c --- /dev/null +++ b/example/simple/patch_updates.cpp @@ -0,0 +1,280 @@ +// +// 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_patch_updates + +/** + * 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. + */ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +/** + * Represents a single update as a name, value pair. + * The idea is to use command-line arguments to compose + * a std::vector with the fields to be updated, + * and use mysql::sequence() to join these with commas + */ +struct update_field +{ + // The field name to set (i.e. the column 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. + mysql::field_view field_value; +}; + +// Contains the parsed command-line arguments +struct cmdline_args +{ + // MySQL username to use during authentication. + std::string_view username; + + // MySQL password to use during authentication. + std::string_view password; + + // Hostname where the MySQL server is listening. + std::string_view server_hostname; + + // The ID of the employee we want to update. + std::int64_t employee_id{}; + + // A list of name, value pairs containing the employee fields to update. + std::vector updates; +}; + +// Parses the command line arguments, calling exit on failure. +static cmdline_args parse_cmdline_args(int argc, char** argv) +{ + // Available options + 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]() { + std::cerr << "Usage: " << argv[0] + << " employee_id [updates]\n"; + exit(1); + }; + + // Check number of arguments + if (argc <= 5) + print_usage_and_exit(); + + // Parse the required arguments + cmdline_args res; + res.username = argv[1]; + res.password = argv[2]; + res.server_hostname = argv[3]; + res.employee_id = std::stoll(argv[4]); + + // Parse the requested updates + for (int i = 5; i < argc; ++i) + { + // Get the argument + std::string_view arg = argv[i]; + + // Attempt to match it with the options we have + if (arg.starts_with(company_id_prefix)) + { + 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)) + { + 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)) + { + 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(std::string(arg.substr(salary_prefix.size()))); + res.updates.push_back(update_field{"salary", mysql::field_view(new_value)}); + } + else + { + std::cerr << "Unrecognized option: " << arg << std::endl; + print_usage_and_exit(); + } + } + + // There should be one update, at least + if (res.updates.empty()) + { + std::cerr << "There should be one update, at least\n"; + print_usage_and_exit(); + } + + return res; +} + +// 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{ + .server_address = mysql::host_and_port(std::string(args.server_hostname)), + .username = std::string(args.username), + .password = std::string(args.password), + .database = "boost_mysql_examples" + }; + + // Connect to the server + 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, 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 + // before sending the query to the server. + // We use sequence() to output the update list separated by commas. + // We want to update the employee and then retrieve it. MySQL doesn't support + // the UPDATE ... RETURNING statement to update and retrieve data atomically, + // so we will use a transaction to guarantee consistency. + // Instead of running every statement separately, we activated params.multi_queries, + // which allows semicolon-separated statements. + // As in std::format, we can use explicit indices like {0} and {1} to reference arguments. + 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", + mysql::sequence(args.updates, update_format_fn), + args.employee_id + ), + result + ); + + // We ran 4 queries, so the results object will hold 4 resultsets. + // Get the rows retrieved by the SELECT (the 3rd one). + auto rws = result.at(2).rows(); + + // If there are no rows, the given employee does not exist. + if (rws.empty()) + { + std::cerr << "employee_id=" << args.employee_id << " not found" << std::endl; + exit(1); + } + + // Print the updated employee. + const auto employee = rws.at(0); + std::cout << "Updated employee with id=" << args.employee_id << ":\n" + << " first_name: " << employee.at(0) << "\n last_name: " << employee.at(1) + << "\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. + 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) +{ + try + { + main_impl(argc, argv); + } + catch (const boost::mysql::error_with_diagnostics& err) + { + // Some errors include additional diagnostics, like server-provided error messages. + // Security note: diagnostics::server_message may contain user-supplied values (e.g. the + // field value that caused the error) and is encoded using to the connection's encoding + // (UTF-8 by default). Treat is as untrusted input. + std::cerr << "Error: " << err.what() << '\n' + << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; + return 1; + } + catch (const std::exception& err) + { + std::cerr << "Error: " << err.what() << std::endl; + return 1; + } +} + +//] + +#else + +#include + +int main() +{ + std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example" + << std::endl; +} + +#endif From af48fbd04992a6c961fb25d4c9519a8e1ad17993 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 13:22:19 +0200 Subject: [PATCH 10/77] Revamp dynamic_filters example --- example/patch_updates.cpp | 239 ----------------------- example/simple/batch_inserts.cpp | 2 - example/simple/batch_inserts_generic.cpp | 2 - example/{ => simple}/dynamic_filters.cpp | 198 ++++++++++--------- example/simple/patch_updates.cpp | 2 - 5 files changed, 105 insertions(+), 338 deletions(-) delete mode 100644 example/patch_updates.cpp rename example/{ => simple}/dynamic_filters.cpp (61%) diff --git a/example/patch_updates.cpp b/example/patch_updates.cpp deleted file mode 100644 index b098784ac..000000000 --- a/example/patch_updates.cpp +++ /dev/null @@ -1,239 +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_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. -// -// Note: client-side SQL formatting is an experimental feature. - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -using boost::mysql::field_view; -using boost::mysql::string_view; - -/** - * Represents a single update as a name, value pair. - * The idea is to use command-line arguments to compose - * a std::vector with the fields to be updated, - * and use mysql::sequence() to join these with commas - */ -struct update_field -{ - // The field name to set (i.e. the column name) - 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; -}; - -// Contains the parsed command-line arguments -struct cmdline_args -{ - // MySQL username to use during authentication. - string_view username; - - // MySQL password to use during authentication. - string_view password; - - // Hostname where the MySQL server is listening. - string_view server_hostname; - - // The ID of the employee we want to update. - std::int64_t employee_id{}; - - // A list of name, value pairs containing the employee fields to update. - std::vector updates; -}; - -// Parses the command line arguments, calling exit on failure. -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="; - - // Helper function to print the usage message and exit - auto print_usage_and_exit = [argv]() { - std::cerr << "Usage: " << argv[0] - << " employee_id [updates]\n"; - exit(1); - }; - - // Check number of arguments - if (argc <= 5) - print_usage_and_exit(); - - // Parse the required arguments - cmdline_args res; - res.username = argv[1]; - res.password = argv[2]; - res.server_hostname = argv[3]; - res.employee_id = std::stoll(argv[4]); - - // Parse the requested updates - for (int i = 5; i < argc; ++i) - { - // Get the argument - 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)}); - } - 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)}); - } - 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)}); - } - 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)}); - } - else - { - std::cerr << "Unrecognized option: " << arg << std::endl; - print_usage_and_exit(); - } - } - - // There should be one update, at least - if (res.updates.empty()) - { - std::cerr << "There should be one update, at least\n"; - print_usage_and_exit(); - } - - return res; -} - -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; - - // 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). - // 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); - params.username = args.username; - params.password = args.password; - params.database = "boost_mysql_examples"; - params.multi_queries = true; - - // Connect to the server - conn.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); - }; - - // Compose and execute the query. with_params will expand placeholders - // before sending the query to the server. - // We use sequence() to output the update list separated by commas. - // We want to update the employee and then retrieve it. MySQL doesn't support - // the UPDATE ... RETURNING statement to update and retrieve data atomically, - // so we will use a transaction to guarantee consistency. - // Instead of running every statement separately, we activated params.multi_queries, - // which allows semicolon-separated statements. - // As in std::format, we can use explicit indices like {0} and {1} to reference arguments. - boost::mysql::results result; - conn.execute( - boost::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(args.updates, update_format_fn), - args.employee_id - ), - result - ); - - // We ran 4 queries, so the results object will hold 4 resultsets. - // Get the rows retrieved by the SELECT (the 3rd one). - auto rws = result.at(2).rows(); - - // If there are no rows, the given employee does not exist. - if (rws.empty()) - { - std::cerr << "employee_id=" << args.employee_id << " not found" << std::endl; - exit(1); - } - - // Print the updated employee. - const auto employee = rws.at(0); - std::cout << "Updated employee with id=" << args.employee_id << ":\n" - << " first_name: " << employee.at(0) << "\n last_name: " << employee.at(1) - << "\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(); -} - -int main(int argc, char** argv) -{ - try - { - main_impl(argc, argv); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // Some errors include additional diagnostics, like server-provided error messages. - // Security note: diagnostics::server_message may contain user-supplied values (e.g. the - // field value that caused the error) and is encoded using to the connection's encoding - // (UTF-8 by default). Treat is as untrusted input. - std::cerr << "Error: " << err.what() << '\n' - << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl; - return 1; - } - catch (const std::exception& err) - { - std::cerr << "Error: " << err.what() << std::endl; - return 1; - } -} - -//] diff --git a/example/simple/batch_inserts.cpp b/example/simple/batch_inserts.cpp index d028dded4..3389b891f 100644 --- a/example/simple/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -5,10 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//<- #include #ifdef BOOST_ASIO_HAS_CO_AWAIT -//-> //[example_batch_inserts diff --git a/example/simple/batch_inserts_generic.cpp b/example/simple/batch_inserts_generic.cpp index ea0f7cc5c..14400a371 100644 --- a/example/simple/batch_inserts_generic.cpp +++ b/example/simple/batch_inserts_generic.cpp @@ -5,10 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//<- #include #ifdef BOOST_ASIO_HAS_CO_AWAIT -//-> //[example_batch_inserts_generic diff --git a/example/dynamic_filters.cpp b/example/simple/dynamic_filters.cpp similarity index 61% rename from example/dynamic_filters.cpp rename to example/simple/dynamic_filters.cpp index 4105ed7b1..6200bb4cc 100644 --- a/example/dynamic_filters.cpp +++ b/example/simple/dynamic_filters.cpp @@ -5,13 +5,20 @@ // 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. -// -// Client-side SQL formatting is an experimental feature. +/** + * 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. + */ #include #include @@ -20,26 +27,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 @@ -59,7 +64,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) { @@ -77,39 +82,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]() { @@ -130,28 +135,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)) { @@ -191,20 +196,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, @@ -216,10 +221,10 @@ std::string compose_get_employees_query( // Add the query with the filters to ctx. // sequence() will invoke filter_format_fn for each element in filts, // using the string " AND " as glue, to separate filters - 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(filts, filter_format_fn, " AND ") + mysql::sequence(filts, filter_format_fn, " AND ") ); // Add the order by @@ -227,7 +232,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 @@ -237,64 +242,59 @@ 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{ + .server_address = mysql::host_and_port(std::string(args.server_hostname)), + .username = std::string(args.username), + .password = std::string(args.password), + .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) @@ -304,7 +304,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(); } @@ -333,3 +333,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/simple/patch_updates.cpp b/example/simple/patch_updates.cpp index 251867a8c..0d412d09f 100644 --- a/example/simple/patch_updates.cpp +++ b/example/simple/patch_updates.cpp @@ -5,10 +5,8 @@ // 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 From bdf746d57dd5ca2eda0b2342916516a01556b78c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 13:35:22 +0200 Subject: [PATCH 11/77] Revamp unix sockets --- example/simple/batch_inserts.cpp | 1 + example/simple/unix_socket.cpp | 131 +++++++++++++++++++++++++++++++ example/unix_socket.cpp | 127 ------------------------------ 3 files changed, 132 insertions(+), 127 deletions(-) create mode 100644 example/simple/unix_socket.cpp delete mode 100644 example/unix_socket.cpp diff --git a/example/simple/batch_inserts.cpp b/example/simple/batch_inserts.cpp index 3389b891f..0170f3c64 100644 --- a/example/simple/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -78,6 +78,7 @@ static 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 server_hostname, std::string username, diff --git a/example/simple/unix_socket.cpp b/example/simple/unix_socket.cpp new file mode 100644 index 000000000..2acae85cc --- /dev/null +++ b/example/simple/unix_socket.cpp @@ -0,0 +1,131 @@ +// +// 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 + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +// The main coroutine +asio::awaitable coro_main(std::string unix_socket_path, std::string username, std::string password) +{ + // 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 + // Instead of a hostname and port, we use a unix_path as server_address + mysql::connect_params params{ + .server_address = mysql::unix_path(std::move(unix_socket_path)), + .username = std::move(username), + .password = std::move(password), + .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); + } + + 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/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; - } -} - -//] From f98d1e868f48e5985ecce5d9677e758dd8e07d70 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 13:36:02 +0200 Subject: [PATCH 12/77] cmake initial revamp --- example/CMakeLists.txt | 241 ++++++++++++++++++++++------------------- 1 file changed, 129 insertions(+), 112 deletions(-) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index fd10c1f10..126428d39 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -25,7 +25,7 @@ target_link_libraries( # 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}) + target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_mysql_examples_common) boost_mysql_common_target_settings(${EXAMPLE_NAME}) endfunction() @@ -41,123 +41,140 @@ function(run_python_example EXAMPLE_TARGET RUNNER_NAME) ) endfunction() -# Build and run an example function(add_example EXAMPLE_NAME EXAMPLE_PATH) - add_example_target(${EXAMPLE_NAME} ${EXAMPLE_PATH}) + set(MULTI_VALUE_ARGS LIBS ARGS) + cmake_parse_arguments(ADD_EXAMPLE "" "" "${MULTI_VALUE_ARGS}" ${ARGN}) + set(TARGET_NAME "boost_mysql_example_${EXAMPLE_NAME}") + add_example_target(${TARGET_NAME} ${EXAMPLE_PATH}) + target_link_libraries(${TARGET_NAME} PRIVATE ${ADD_EXAMPLE_LIBS}) add_test( - NAME ${EXAMPLE_NAME} - COMMAND ${EXAMPLE_NAME} ${ARGN} + NAME ${TARGET_NAME} + COMMAND ${TARGET_NAME} ${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} "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) - -# 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 - ) -endif() - -# Source script -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 -) -boost_mysql_common_target_settings(boost_mysql_example_connection_pool) -run_python_example(boost_mysql_example_connection_pool run_connection_pool.py) +# 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() + +set(REGULAR_ARGS "example_user example_password ${SERVER_HOST}") + +# Tutorials +add_example(tutorial tutorial.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_async tutorial_async.cpp ARGS ${REGULAR_ARGS}) + +# 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 ${REGULAR_ARGS} LIBS Boost::json) +add_simple_example(batch_inserts_generic ARGS ${REGULAR_ARGS} LIBS Boost::json) +add_simple_example(patch_updates ARGS ${REGULAR_ARGS}) +add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) +add_simple_example(unix_socket ARGS example_user example_password) + + + +# # Regular examples are the ones that require no extra linking libs and can be run +# # with example_user example_password +# set(REGULAR_EXAMPLES +# tutorial +# tutorial_async +# simple/callbacks +# simple/ +# 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) + +# # 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 +# ) +# endif() + +# # Source script +# 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 +# ) +# boost_mysql_common_target_settings(boost_mysql_example_connection_pool) +# run_python_example(boost_mysql_example_connection_pool run_connection_pool.py) From 1924b70a77a878cb93fab445826043d87f6a0401 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 13:44:35 +0200 Subject: [PATCH 13/77] New disable_tls example --- example/simple/disable_tls.cpp | 130 +++++++++++++++++++++++++++++++++ example/simple/unix_socket.cpp | 3 +- example/tutorial_async.cpp | 3 +- 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 example/simple/disable_tls.cpp diff --git a/example/simple/disable_tls.cpp b/example/simple/disable_tls.cpp new file mode 100644 index 000000000..c703a950c --- /dev/null +++ b/example/simple/disable_tls.cpp @@ -0,0 +1,130 @@ +// +// 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. + */ + +#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 server_hostname, std::string username, std::string password) +{ + // 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. + // Passing ssl_mode::disable will disable the use of TLS. + mysql::connect_params params{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples", + .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/system doesn't have the required capabilities to run this example" + << std::endl; +} + +#endif diff --git a/example/simple/unix_socket.cpp b/example/simple/unix_socket.cpp index 2acae85cc..50c9f84db 100644 --- a/example/simple/unix_socket.cpp +++ b/example/simple/unix_socket.cpp @@ -41,7 +41,8 @@ asio::awaitable coro_main(std::string unix_socket_path, std::string userna mysql::any_connection conn(co_await asio::this_coro::executor); // The socket path, username, password and database to use - // Instead of a hostname and port, we use a unix_path as server_address + // Instead of a hostname and port, we use a unix_path as server_address. + // UNIX socket connections never use TLS. mysql::connect_params params{ .server_address = mysql::unix_path(std::move(unix_socket_path)), .username = std::move(username), diff --git a/example/tutorial_async.cpp b/example/tutorial_async.cpp index 8335a5244..1e113cd09 100644 --- a/example/tutorial_async.cpp +++ b/example/tutorial_async.cpp @@ -50,7 +50,8 @@ asio::awaitable coro_main(std::string server_hostname, std::string usernam // 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 + // The hostname, username, password and database to use. + // TLS is used by default. mysql::connect_params params{ .server_address = mysql::host_and_port{std::move(server_hostname)}, .username = std::move(username), From 6dc4741f26820bb0b617a7e096d2ccaff9d94523 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 14:03:52 +0200 Subject: [PATCH 14/77] Revamp SSL example --- example/simple/batch_inserts.cpp | 4 +- example/simple/disable_tls.cpp | 2 +- .../tls_certificate_verification.cpp} | 153 ++++++++++-------- 3 files changed, 93 insertions(+), 66 deletions(-) rename example/{ssl.cpp => simple/tls_certificate_verification.cpp} (52%) diff --git a/example/simple/batch_inserts.cpp b/example/simple/batch_inserts.cpp index 0170f3c64..88472b64e 100644 --- a/example/simple/batch_inserts.cpp +++ b/example/simple/batch_inserts.cpp @@ -88,10 +88,10 @@ asio::awaitable coro_main( { // Create a connection. // Will use the same executor as the coroutine. - boost::mysql::any_connection conn(co_await asio::this_coro::executor); + mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - boost::mysql::connect_params params{ + mysql::connect_params params{ .server_address = mysql::host_and_port(std::move(server_hostname)), .username = std::move(username), .password = std::move(password), diff --git a/example/simple/disable_tls.cpp b/example/simple/disable_tls.cpp index c703a950c..e6e034708 100644 --- a/example/simple/disable_tls.cpp +++ b/example/simple/disable_tls.cpp @@ -123,7 +123,7 @@ int main(int argc, char** argv) int main() { - std::cout << "Sorry, your compiler/system doesn't have the required capabilities to run this example" + std::cout << "Sorry, your compiler doesn't have the required capabilities to run this example" << std::endl; } diff --git a/example/ssl.cpp b/example/simple/tls_certificate_verification.cpp similarity index 52% rename from example/ssl.cpp rename to example/simple/tls_certificate_verification.cpp index 868d59fec..785159895 100644 --- a/example/ssl.cpp +++ b/example/simple/tls_certificate_verification.cpp @@ -5,27 +5,37 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//[example_ssl +#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. + */ + +#include #include #include #include -#include +#include #include +#include #include -#include #include #include +#include #include -#define ASSERT(expr) \ - if (!(expr)) \ - { \ - std::cerr << "Assertion failed: " #expr << std::endl; \ - exit(1); \ - } +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----- @@ -51,78 +61,83 @@ OzBrmpfHEhF6NDU= -----END CERTIFICATE----- )%"; -void print_employee(boost::mysql::row_view employee) +// The main coroutine +asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) { - 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); + // Create a SSL context, which contains TLS configuration options + asio::ssl::context ssl_ctx(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); + // 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/ - ssl_ctx.add_certificate_authority(boost::asio::buffer(CA_PEM)); + 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. - ssl_ctx.set_verify_callback(boost::asio::ssl::host_name_verification("mysql")); + 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_context = &ssl_ctx} + ); - // 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); + // The hostname, username, password and database to use + mysql::connect_params params{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples" + }; - // Hostname resolution - auto endpoints = resolver.resolve(argv[3], boost::mysql::default_port_string); + // Connect to the server + co_await conn.async_connect(params); - // 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); + // 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; - // 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); + // Notify the MySQL server we want to quit, then close the underlying connection. + co_await conn.async_close(); +} - for (auto employee : result.rows()) +void main_impl(int argc, char** argv) +{ + if (argc != 4) { - print_employee(employee); + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); } - // Cleanup - conn.close(); + // 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) @@ -149,3 +164,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 \ No newline at end of file From b00fa54ba400b48200fa929bd4c81485dc0f6980 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 14:04:01 +0200 Subject: [PATCH 15/77] Fix problem with args in cmake --- example/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 126428d39..362896108 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -50,7 +50,7 @@ function(add_example EXAMPLE_NAME EXAMPLE_PATH) target_link_libraries(${TARGET_NAME} PRIVATE ${ADD_EXAMPLE_LIBS}) add_test( NAME ${TARGET_NAME} - COMMAND ${TARGET_NAME} ${ARGN} + COMMAND ${TARGET_NAME} ${ADD_EXAMPLE_ARGS} ) endfunction() @@ -64,7 +64,7 @@ endfunction() # run_python_example(${EXAMPLE_NAME} run_stored_procedures.py) # endfunction() -set(REGULAR_ARGS "example_user example_password ${SERVER_HOST}") +set(REGULAR_ARGS example_user example_password ${SERVER_HOST}) # Tutorials add_example(tutorial tutorial.cpp ARGS ${REGULAR_ARGS}) @@ -78,6 +78,8 @@ add_simple_example(batch_inserts_generic ARGS ${REGULAR_ARGS} LIBS Boost::json add_simple_example(patch_updates ARGS ${REGULAR_ARGS}) add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) add_simple_example(unix_socket ARGS example_user example_password) +add_simple_example(disable_tls ARGS ${REGULAR_ARGS}) +add_simple_example(tls_certificate_verification ARGS ${REGULAR_ARGS}) From 618d0a6fc7e85a3a4cc73d6b33f4341e025d19f7 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 17:23:00 +0200 Subject: [PATCH 16/77] Metadata example --- example/CMakeLists.txt | 1 + example/metadata.cpp | 130 -------------------------- example/simple/metadata.cpp | 178 ++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 130 deletions(-) delete mode 100644 example/metadata.cpp create mode 100644 example/simple/metadata.cpp diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 362896108..4b51dc118 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -80,6 +80,7 @@ add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) add_simple_example(unix_socket ARGS example_user example_password) add_simple_example(disable_tls ARGS ${REGULAR_ARGS}) add_simple_example(tls_certificate_verification ARGS ${REGULAR_ARGS}) +add_simple_example(metadata ARGS ${REGULAR_ARGS}) 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/simple/metadata.cpp b/example/simple/metadata.cpp new file mode 100644 index 000000000..08336170b --- /dev/null +++ b/example/simple/metadata.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) +// + +#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. + */ + +#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 server_hostname, std::string username, std::string 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. + // Passing ssl_mode::disable will disable the use of TLS. + mysql::connect_params params{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples", + .ssl = mysql::ssl_mode::disable, + }; + + // 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 From 86ac37fc07857284a59482958738b5af2b21b54e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 17:44:11 +0200 Subject: [PATCH 17/77] Prepared statements example --- example/CMakeLists.txt | 19 +-- example/simple/prepared_statements.cpp | 170 +++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 example/simple/prepared_statements.cpp diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 4b51dc118..55be3c219 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -71,16 +71,17 @@ add_example(tutorial tutorial.cpp ARGS ${REGULAR_ARGS}) add_example(tutorial_async tutorial_async.cpp ARGS ${REGULAR_ARGS}) # 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 ${REGULAR_ARGS} LIBS Boost::json) -add_simple_example(batch_inserts_generic ARGS ${REGULAR_ARGS} LIBS Boost::json) -add_simple_example(patch_updates ARGS ${REGULAR_ARGS}) -add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) -add_simple_example(unix_socket ARGS example_user example_password) -add_simple_example(disable_tls ARGS ${REGULAR_ARGS}) +add_simple_example(callbacks ARGS ${REGULAR_ARGS}) +add_simple_example(coroutines_cpp11 ARGS ${REGULAR_ARGS} LIBS Boost::context) +add_simple_example(batch_inserts ARGS ${REGULAR_ARGS} LIBS Boost::json) +add_simple_example(batch_inserts_generic ARGS ${REGULAR_ARGS} LIBS Boost::json) +add_simple_example(patch_updates ARGS ${REGULAR_ARGS}) +add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) +add_simple_example(unix_socket ARGS example_user example_password) +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(metadata ARGS ${REGULAR_ARGS}) +add_simple_example(prepared_statements ARGS ${REGULAR_ARGS} "HGS") diff --git a/example/simple/prepared_statements.cpp b/example/simple/prepared_statements.cpp new file mode 100644 index 000000000..5db9cce48 --- /dev/null +++ b/example/simple/prepared_statements.cpp @@ -0,0 +1,170 @@ +// +// 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 +#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 server_hostname, + std::string username, + std::string 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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .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 From 90e99ba55f15432be0e20a84a49d717796fe2c29 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 18:02:16 +0200 Subject: [PATCH 18/77] Move examples --- example/{tutorial.cpp => 1_tutorial/1_sync.cpp} | 0 example/{tutorial_async.cpp => 1_tutorial/2_async.cpp} | 0 example/{simple => 2_simple}/batch_inserts.cpp | 0 example/{simple => 2_simple}/batch_inserts_generic.cpp | 0 example/{simple => 2_simple}/callbacks.cpp | 0 example/{simple => 2_simple}/coroutines_cpp11.cpp | 0 example/{simple => 2_simple}/disable_tls.cpp | 0 example/{simple => 2_simple}/dynamic_filters.cpp | 0 example/{simple => 2_simple}/metadata.cpp | 0 example/{simple => 2_simple}/patch_updates.cpp | 0 example/{simple => 2_simple}/prepared_statements.cpp | 0 .../{simple => 2_simple}/tls_certificate_verification.cpp | 0 example/{simple => 2_simple}/unix_socket.cpp | 0 example/CMakeLists.txt | 6 +++--- 14 files changed, 3 insertions(+), 3 deletions(-) rename example/{tutorial.cpp => 1_tutorial/1_sync.cpp} (100%) rename example/{tutorial_async.cpp => 1_tutorial/2_async.cpp} (100%) rename example/{simple => 2_simple}/batch_inserts.cpp (100%) rename example/{simple => 2_simple}/batch_inserts_generic.cpp (100%) rename example/{simple => 2_simple}/callbacks.cpp (100%) rename example/{simple => 2_simple}/coroutines_cpp11.cpp (100%) rename example/{simple => 2_simple}/disable_tls.cpp (100%) rename example/{simple => 2_simple}/dynamic_filters.cpp (100%) rename example/{simple => 2_simple}/metadata.cpp (100%) rename example/{simple => 2_simple}/patch_updates.cpp (100%) rename example/{simple => 2_simple}/prepared_statements.cpp (100%) rename example/{simple => 2_simple}/tls_certificate_verification.cpp (100%) rename example/{simple => 2_simple}/unix_socket.cpp (100%) diff --git a/example/tutorial.cpp b/example/1_tutorial/1_sync.cpp similarity index 100% rename from example/tutorial.cpp rename to example/1_tutorial/1_sync.cpp diff --git a/example/tutorial_async.cpp b/example/1_tutorial/2_async.cpp similarity index 100% rename from example/tutorial_async.cpp rename to example/1_tutorial/2_async.cpp diff --git a/example/simple/batch_inserts.cpp b/example/2_simple/batch_inserts.cpp similarity index 100% rename from example/simple/batch_inserts.cpp rename to example/2_simple/batch_inserts.cpp diff --git a/example/simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp similarity index 100% rename from example/simple/batch_inserts_generic.cpp rename to example/2_simple/batch_inserts_generic.cpp diff --git a/example/simple/callbacks.cpp b/example/2_simple/callbacks.cpp similarity index 100% rename from example/simple/callbacks.cpp rename to example/2_simple/callbacks.cpp diff --git a/example/simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp similarity index 100% rename from example/simple/coroutines_cpp11.cpp rename to example/2_simple/coroutines_cpp11.cpp diff --git a/example/simple/disable_tls.cpp b/example/2_simple/disable_tls.cpp similarity index 100% rename from example/simple/disable_tls.cpp rename to example/2_simple/disable_tls.cpp diff --git a/example/simple/dynamic_filters.cpp b/example/2_simple/dynamic_filters.cpp similarity index 100% rename from example/simple/dynamic_filters.cpp rename to example/2_simple/dynamic_filters.cpp diff --git a/example/simple/metadata.cpp b/example/2_simple/metadata.cpp similarity index 100% rename from example/simple/metadata.cpp rename to example/2_simple/metadata.cpp diff --git a/example/simple/patch_updates.cpp b/example/2_simple/patch_updates.cpp similarity index 100% rename from example/simple/patch_updates.cpp rename to example/2_simple/patch_updates.cpp diff --git a/example/simple/prepared_statements.cpp b/example/2_simple/prepared_statements.cpp similarity index 100% rename from example/simple/prepared_statements.cpp rename to example/2_simple/prepared_statements.cpp diff --git a/example/simple/tls_certificate_verification.cpp b/example/2_simple/tls_certificate_verification.cpp similarity index 100% rename from example/simple/tls_certificate_verification.cpp rename to example/2_simple/tls_certificate_verification.cpp diff --git a/example/simple/unix_socket.cpp b/example/2_simple/unix_socket.cpp similarity index 100% rename from example/simple/unix_socket.cpp rename to example/2_simple/unix_socket.cpp diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 55be3c219..0655ccc43 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -55,7 +55,7 @@ function(add_example EXAMPLE_NAME EXAMPLE_PATH) endfunction() function(add_simple_example EXAMPLE_NAME) - add_example(${EXAMPLE_NAME} "simple/${EXAMPLE_NAME}.cpp" ${ARGN}) + add_example(${EXAMPLE_NAME} "2_simple/${EXAMPLE_NAME}.cpp" ${ARGN}) endfunction() # The order management examples must be run several times through a Python script @@ -67,8 +67,8 @@ endfunction() set(REGULAR_ARGS example_user example_password ${SERVER_HOST}) # Tutorials -add_example(tutorial tutorial.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_async tutorial_async.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) # Simple add_simple_example(callbacks ARGS ${REGULAR_ARGS}) From 073d54e7e5f2e7b501a0b7c373ee992f173ab7f6 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 18:12:34 +0200 Subject: [PATCH 19/77] with_params tutorial --- example/1_tutorial/2_async.cpp | 28 ++--- example/1_tutorial/3_with_params.cpp | 151 +++++++++++++++++++++++++++ example/CMakeLists.txt | 5 +- 3 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 example/1_tutorial/3_with_params.cpp diff --git a/example/1_tutorial/2_async.cpp b/example/1_tutorial/2_async.cpp index 1e113cd09..6163f8294 100644 --- a/example/1_tutorial/2_async.cpp +++ b/example/1_tutorial/2_async.cpp @@ -7,11 +7,10 @@ // TODO: link this -//<- #include - #ifdef BOOST_ASIO_HAS_CO_AWAIT -//-> + +//[bla #include #include @@ -104,17 +103,6 @@ void main_impl(int argc, char** argv) 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 @@ -139,3 +127,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 \ 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..b9f421958 --- /dev/null +++ b/example/1_tutorial/3_with_params.cpp @@ -0,0 +1,151 @@ +// +// 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) +// + +// TODO: link this + +#include +#ifdef BOOST_ASIO_HAS_CO_AWAIT + +//[bla + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mysql = boost::mysql; +namespace asio = boost::asio; + +/** + * This example shows how to issue queries with parameters containing + * untrusted input securely. Given an employee ID, it prints their first name. + * The example builds on the previous async tutorial. + */ +asio::awaitable coro_main( + std::string server_hostname, + std::string username, + std::string 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{ + .server_address = mysql::host_and_port{std::move(server_hostname)}, + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples" + }; + + // Connect to the server + co_await conn.async_connect(params); + + // 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 FROM employee WHERE id = {}", employee_id), + result + ); + + // Did we find an employee with that ID? + if (result.rows().empty()) + { + std::cout << "Employee not found" << std::endl; + } + else + { + // Print the first field in the first row + std::cout << "Employee's name is: " << 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 != 5) + { + std::cerr << "Usage: " << argv[0] << " \n"; + exit(1); + } + + // The execution context, required to run I/O operations. + asio::io_context ctx; + + // The entry point. We pass in a function returning + // boost::asio::awaitable, as required. + // Calling co_spawn enqueues the coroutine for execution. + asio::co_spawn( + ctx, + [argv] { return coro_main(argv[3], argv[1], argv[2], std::stoi(argv[4])); }, + // If any exception is thrown in the coroutine body, rethrow it. + [](std::exception_ptr ptr) { + if (ptr) + { + std::rethrow_exception(ptr); + } + } + ); + + // Calling run will actually execute the coroutine until completion + 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/CMakeLists.txt b/example/CMakeLists.txt index 0655ccc43..1ba212ef9 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -67,8 +67,9 @@ endfunction() set(REGULAR_ARGS example_user example_password ${SERVER_HOST}) # Tutorials -add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_with_params 1_tutorial/3_with_params.cpp ARGS ${REGULAR_ARGS} 1) # Simple add_simple_example(callbacks ARGS ${REGULAR_ARGS}) From f8f25a5b9d591c60912ee71e09e94fd062bfc24c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 18:49:35 +0200 Subject: [PATCH 20/77] Example on timeouts --- example/2_simple/timeouts.cpp | 170 ++++++++++++++++++++++++++++++++++ example/CMakeLists.txt | 1 + example/timeouts.cpp | 167 --------------------------------- 3 files changed, 171 insertions(+), 167 deletions(-) create mode 100644 example/2_simple/timeouts.cpp delete mode 100644 example/timeouts.cpp diff --git a/example/2_simple/timeouts.cpp b/example/2_simple/timeouts.cpp new file mode 100644 index 000000000..112269bac --- /dev/null +++ b/example/2_simple/timeouts.cpp @@ -0,0 +1,170 @@ +// +// 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 +#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 + +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 server_hostname, + std::string username, + std::string 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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .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/CMakeLists.txt b/example/CMakeLists.txt index 1ba212ef9..0692c2a91 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -83,6 +83,7 @@ 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") 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; - } -} - -//] From 316f5d6f29096246b516b8bc3d7954c724e2a526 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 18:57:11 +0200 Subject: [PATCH 21/77] timeouts includes --- example/2_simple/timeouts.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/example/2_simple/timeouts.cpp b/example/2_simple/timeouts.cpp index 112269bac..42b15b105 100644 --- a/example/2_simple/timeouts.cpp +++ b/example/2_simple/timeouts.cpp @@ -5,13 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include -#include -#include - #include - -#include #ifdef BOOST_ASIO_HAS_CO_AWAIT //[example_timeouts @@ -27,8 +21,11 @@ * Timeouts can't be used with sync functions. */ +#include #include +#include #include +#include #include #include @@ -38,6 +35,7 @@ #include #include #include +#include namespace asio = boost::asio; namespace mysql = boost::mysql; From b75df049509371678e6285bed62bd56f05133e55 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 19:09:44 +0200 Subject: [PATCH 22/77] Pipeline example --- example/2_simple/pipeline.cpp | 204 +++++++++++++++++++++++++++++++++ example/CMakeLists.txt | 1 + example/pipeline.cpp | 208 ---------------------------------- 3 files changed, 205 insertions(+), 208 deletions(-) create mode 100644 example/2_simple/pipeline.cpp delete mode 100644 example/pipeline.cpp diff --git a/example/2_simple/pipeline.cpp b/example/2_simple/pipeline.cpp new file mode 100644 index 000000000..ffa1edc6e --- /dev/null +++ b/example/2_simple/pipeline.cpp @@ -0,0 +1,204 @@ +// +// 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. + */ + +#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. Using as_tuple prevents async_run_pipeline from throwing. + // This allows us to include the diagnostics object diag in the thrown exception. + // 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 server_hostname, + std::string username, + std::string 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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .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/CMakeLists.txt b/example/CMakeLists.txt index 0692c2a91..f1634800e 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -84,6 +84,7 @@ 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") diff --git a/example/pipeline.cpp b/example/pipeline.cpp deleted file mode 100644 index f1d039df5..000000000 --- a/example/pipeline.cpp +++ /dev/null @@ -1,208 +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. Using as_tuple prevents async_run_pipeline from throwing. - // This allows us to include the diagnostics object diag in the thrown exception. - // 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 - -//] From d09efe3ff5355bcfafc3d5927c501982271f80e1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 11 Oct 2024 19:32:22 +0200 Subject: [PATCH 23/77] multi_function example --- example/2_simple/multi_function.cpp | 149 +++++++++++++++++++++++ example/2_simple/prepared_statements.cpp | 4 - example/CMakeLists.txt | 1 + 3 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 example/2_simple/multi_function.cpp diff --git a/example/2_simple/multi_function.cpp b/example/2_simple/multi_function.cpp new file mode 100644 index 000000000..9355e309b --- /dev/null +++ b/example/2_simple/multi_function.cpp @@ -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) +// + +#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 server_hostname, std::string username, std::string 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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .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/prepared_statements.cpp b/example/2_simple/prepared_statements.cpp index 5db9cce48..71564dd2c 100644 --- a/example/2_simple/prepared_statements.cpp +++ b/example/2_simple/prepared_statements.cpp @@ -22,17 +22,13 @@ #include #include -#include #include #include #include #include -#include #include #include -#include -#include #include #include diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index f1634800e..af66a54b2 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -85,6 +85,7 @@ 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}) From e0e617f4c3c7ebe190943575f3f4f5fffebaf662 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 12 Oct 2024 18:50:01 +0200 Subject: [PATCH 24/77] Unused include --- example/2_simple/batch_inserts_generic.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/2_simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp index b7d20fba7..f1c9609fd 100644 --- a/example/2_simple/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -6,8 +6,6 @@ // #include - -#include #ifdef BOOST_ASIO_HAS_CO_AWAIT //[example_batch_inserts_generic From 2a04a292d91493aa443e0da843a6e019a43afa7e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 12 Oct 2024 20:00:00 +0200 Subject: [PATCH 25/77] example on transactions --- example/2_simple/transactions.cpp | 188 +++++++++++++++++++ example/CMakeLists.txt | 1 + example/db_setup.sql | 297 ++++++++++++++++++++++++++++++ 3 files changed, 486 insertions(+) create mode 100644 example/2_simple/transactions.cpp diff --git a/example/2_simple/transactions.cpp b/example/2_simple/transactions.cpp new file mode 100644 index 000000000..64433cc23 --- /dev/null +++ b/example/2_simple/transactions.cpp @@ -0,0 +1,188 @@ +// +// 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_transactions + +/** + * This program shows how to start, commit and roll back transactions + * spanning multiple queries. + * + * The program modifies an order of an online store system, + * adding a new line item to it. The program must first check + * that the order is an editable state. + * + * 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. + */ + +#include +#include +#include +#include +#include +#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 server_hostname, + std::string username, + std::string password, + std::uint64_t order_id, + std::uint64_t product_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{ + .server_address = mysql::host_and_port(std::move(server_hostname)), + .username = std::move(username), + .password = std::move(password), + .database = "boost_mysql_examples", + .multi_queries = true, // Enable support for semicolon-separated queries + }; + + // Connect to the server + co_await conn.async_connect(params); + + // Open a transaction block with START TRANSACTION. + // Transaction statements are regular SQL, and can be issued using async_execute. + // Then retrieve the order and lock it so it doesn't get modified while we're examining it. + // We need to check whether the order is in a 'draft' state before adding items to it. + // We combine the transaction and the select statements to save round-trips to the server. + mysql::results result; + co_await conn.async_execute( + mysql::with_params( + "START TRANSACTION; " + "SELECT status FROM orders WHERE id = {} FOR SHARE", + order_id + ), + result + ); + + // We issued 2 queries, so we get 2 resultsets back. + // The 1st resultset corresponds to the START TRANSACTION and is empty. + // The 2nd resultset corresponds to the SELECT and contains our order. + // If a connection closes while a transaction is in progress, + // the transaction is rolled back. No ROLLBACK statement required. + mysql::rows_view orders = result.at(1).rows(); + if (orders.empty()) + { + std::cout << "Can't find order with id=" << order_id << std::endl; + exit(1); + } + + // Retrieve and check the order status + auto order_status = orders.at(0).at(0).as_string(); + if (order_status != "draft") + { + std::cout << "Order can't be modified because it's in " << order_status << " status\n"; + exit(1); + } + + // We're good to proceed. Insert the new order item and commit the transaction. + // If the INSERT fails, the COMMIT statement is not executed + // and the transaction is rolled back when the connection closes. + co_await conn.async_execute( + mysql::with_params( + "INSERT INTO order_items (order_id, product_id, quantity) VALUES ({}, {}, 1); " + "COMMIT", + order_id, + product_id + ), + 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 != 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]), std::stoi(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 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/CMakeLists.txt b/example/CMakeLists.txt index af66a54b2..2e31d9977 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -86,6 +86,7 @@ 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(transactions ARGS ${REGULAR_ARGS} 1 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.") +; From 9cc45df647f5b902bcd0f7086b31ebdf5be2dddc Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 29 Oct 2024 19:56:30 +0100 Subject: [PATCH 26/77] Relink examples --- example/1_tutorial/1_sync.cpp | 2 +- example/1_tutorial/2_async.cpp | 4 +--- example/1_tutorial/3_with_params.cpp | 4 +--- example/2_simple/callbacks.cpp | 2 +- example/2_simple/coroutines_cpp11.cpp | 2 +- example/{ => 3_advanced}/connection_pool/handle_request.cpp | 0 example/{ => 3_advanced}/connection_pool/handle_request.hpp | 4 ++-- example/{ => 3_advanced}/connection_pool/log_error.hpp | 4 ++-- example/{ => 3_advanced}/connection_pool/main.cpp | 0 example/{ => 3_advanced}/connection_pool/repository.cpp | 0 example/{ => 3_advanced}/connection_pool/repository.hpp | 4 ++-- example/{ => 3_advanced}/connection_pool/server.cpp | 0 example/{ => 3_advanced}/connection_pool/server.hpp | 4 ++-- example/{ => 3_advanced}/connection_pool/types.hpp | 4 ++-- example/{ => 3_advanced}/source_script.cpp | 0 15 files changed, 15 insertions(+), 19 deletions(-) rename example/{ => 3_advanced}/connection_pool/handle_request.cpp (100%) rename example/{ => 3_advanced}/connection_pool/handle_request.hpp (85%) rename example/{ => 3_advanced}/connection_pool/log_error.hpp (90%) rename example/{ => 3_advanced}/connection_pool/main.cpp (100%) rename example/{ => 3_advanced}/connection_pool/repository.cpp (100%) rename example/{ => 3_advanced}/connection_pool/repository.hpp (94%) rename example/{ => 3_advanced}/connection_pool/server.cpp (100%) rename example/{ => 3_advanced}/connection_pool/server.hpp (91%) rename example/{ => 3_advanced}/connection_pool/types.hpp (94%) rename example/{ => 3_advanced}/source_script.cpp (100%) diff --git a/example/1_tutorial/1_sync.cpp b/example/1_tutorial/1_sync.cpp index d4a43b9c9..aa21160cb 100644 --- a/example/1_tutorial/1_sync.cpp +++ b/example/1_tutorial/1_sync.cpp @@ -5,7 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//[tutorial_listing +//[example_tutorial_sync #include #include diff --git a/example/1_tutorial/2_async.cpp b/example/1_tutorial/2_async.cpp index 6163f8294..3ec07f1f1 100644 --- a/example/1_tutorial/2_async.cpp +++ b/example/1_tutorial/2_async.cpp @@ -5,12 +5,10 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// TODO: link this - #include #ifdef BOOST_ASIO_HAS_CO_AWAIT -//[bla +//[example_tutorial_async #include #include diff --git a/example/1_tutorial/3_with_params.cpp b/example/1_tutorial/3_with_params.cpp index b9f421958..3ad2c7bd0 100644 --- a/example/1_tutorial/3_with_params.cpp +++ b/example/1_tutorial/3_with_params.cpp @@ -5,12 +5,10 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// TODO: link this - #include #ifdef BOOST_ASIO_HAS_CO_AWAIT -//[bla +//[example_tutorial_with_params #include #include diff --git a/example/2_simple/callbacks.cpp b/example/2_simple/callbacks.cpp index 1b2196329..f6bf0dfb0 100644 --- a/example/2_simple/callbacks.cpp +++ b/example/2_simple/callbacks.cpp @@ -5,7 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//[example_async_callbacks +//[example_callbacks #include #include diff --git a/example/2_simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp index 56fa8ba10..9e62431c5 100644 --- a/example/2_simple/coroutines_cpp11.cpp +++ b/example/2_simple/coroutines_cpp11.cpp @@ -5,7 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -//[example_async_coroutines +//[example_coroutines_cpp11 /** * This example demonstrates how to use stackful coroutines when using async functions. 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 100% rename from example/connection_pool/main.cpp rename to example/3_advanced/connection_pool/main.cpp 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/source_script.cpp b/example/3_advanced/source_script.cpp similarity index 100% rename from example/source_script.cpp rename to example/3_advanced/source_script.cpp From 30aab13d688f974be10d2b47337358de0dfe153e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 29 Oct 2024 20:16:48 +0100 Subject: [PATCH 27/77] Update example comments --- example/2_simple/batch_inserts.cpp | 3 ++ example/2_simple/batch_inserts_generic.cpp | 3 ++ example/2_simple/callbacks.cpp | 14 +++---- example/2_simple/coroutines_cpp11.cpp | 5 ++- example/2_simple/disable_tls.cpp | 3 ++ example/2_simple/dynamic_filters.cpp | 3 ++ example/2_simple/metadata.cpp | 3 ++ example/2_simple/patch_updates.cpp | 3 ++ example/2_simple/pipeline.cpp | 3 ++ .../2_simple/tls_certificate_verification.cpp | 11 +++++ example/2_simple/unix_socket.cpp | 2 + example/3_advanced/connection_pool/main.cpp | 40 +++++++++---------- 12 files changed, 65 insertions(+), 28 deletions(-) diff --git a/example/2_simple/batch_inserts.cpp b/example/2_simple/batch_inserts.cpp index 15e254c00..7657e0618 100644 --- a/example/2_simple/batch_inserts.cpp +++ b/example/2_simple/batch_inserts.cpp @@ -21,6 +21,9 @@ * 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 diff --git a/example/2_simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp index f1c9609fd..98f24feba 100644 --- a/example/2_simple/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -22,6 +22,9 @@ * 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 diff --git a/example/2_simple/callbacks.cpp b/example/2_simple/callbacks.cpp index f6bf0dfb0..4c975e25e 100644 --- a/example/2_simple/callbacks.cpp +++ b/example/2_simple/callbacks.cpp @@ -7,6 +7,13 @@ //[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 @@ -21,13 +28,6 @@ #include #include -/** - * 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. - */ - // When using callbacks, we usually employ error codes instead of exceptions. using boost::system::error_code; diff --git a/example/2_simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp index 9e62431c5..bef4d5afb 100644 --- a/example/2_simple/coroutines_cpp11.cpp +++ b/example/2_simple/coroutines_cpp11.cpp @@ -12,7 +12,10 @@ * 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 + * 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 diff --git a/example/2_simple/disable_tls.cpp b/example/2_simple/disable_tls.cpp index e6e034708..57fcc2248 100644 --- a/example/2_simple/disable_tls.cpp +++ b/example/2_simple/disable_tls.cpp @@ -16,6 +16,9 @@ * 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 diff --git a/example/2_simple/dynamic_filters.cpp b/example/2_simple/dynamic_filters.cpp index fd1d2d355..e8a0342d5 100644 --- a/example/2_simple/dynamic_filters.cpp +++ b/example/2_simple/dynamic_filters.cpp @@ -18,6 +18,9 @@ * 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 diff --git a/example/2_simple/metadata.cpp b/example/2_simple/metadata.cpp index 08336170b..59b0981b6 100644 --- a/example/2_simple/metadata.cpp +++ b/example/2_simple/metadata.cpp @@ -17,6 +17,9 @@ * 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 diff --git a/example/2_simple/patch_updates.cpp b/example/2_simple/patch_updates.cpp index 31d8265d4..cf5bc2be5 100644 --- a/example/2_simple/patch_updates.cpp +++ b/example/2_simple/patch_updates.cpp @@ -21,6 +21,9 @@ * 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 diff --git a/example/2_simple/pipeline.cpp b/example/2_simple/pipeline.cpp index ffa1edc6e..e22361b07 100644 --- a/example/2_simple/pipeline.cpp +++ b/example/2_simple/pipeline.cpp @@ -19,6 +19,9 @@ * 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 diff --git a/example/2_simple/tls_certificate_verification.cpp b/example/2_simple/tls_certificate_verification.cpp index 785159895..3a4ad929f 100644 --- a/example/2_simple/tls_certificate_verification.cpp +++ b/example/2_simple/tls_certificate_verification.cpp @@ -17,6 +17,13 @@ * 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". + * + * TODO: this should probably be setting TLS SNI */ #include @@ -75,10 +82,14 @@ asio::awaitable coro_main(std::string server_hostname, std::string usernam // 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 hostname you expect in your certificate's common name + // TODO: we could set this to server_hostname and disable the test in some systems ssl_ctx.set_verify_callback(asio::ssl::host_name_verification("mysql")); // Create a connection. diff --git a/example/2_simple/unix_socket.cpp b/example/2_simple/unix_socket.cpp index 50c9f84db..3cdd66c93 100644 --- a/example/2_simple/unix_socket.cpp +++ b/example/2_simple/unix_socket.cpp @@ -70,6 +70,8 @@ void main_impl(int argc, char** argv) 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 diff --git a/example/3_advanced/connection_pool/main.cpp b/example/3_advanced/connection_pool/main.cpp index 01df7d817..77923de59 100644 --- a/example/3_advanced/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 From f99a19d6db1f3861f2a78bc56fe27166d75e11f4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 29 Oct 2024 20:17:36 +0100 Subject: [PATCH 28/77] Redo and automate example page --- doc/qbk/24_examples.qbk | 470 ---------------------------------- doc/qbk/25_examples.qbk | 339 ++++++++++++++++++++++++ tools/scripts/examples_qbk.py | 186 ++++++++++++++ 3 files changed, 525 insertions(+), 470 deletions(-) delete mode 100644 doc/qbk/24_examples.qbk create mode 100644 doc/qbk/25_examples.qbk create mode 100644 tools/scripts/examples_qbk.py 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/25_examples.qbk b/doc/qbk/25_examples.qbk new file mode 100644 index 000000000..5510c473c --- /dev/null +++ b/doc/qbk/25_examples.qbk @@ -0,0 +1,339 @@ +[/ + 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: connections and sync functions] +* [link mysql.examples.tutorial_async Tutorial 2: going async with C++20 coroutines] +* [link mysql.examples.tutorial_with_params Tutorial 3: queries with parameters] + +[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.transactions Using 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.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: connections and sync functions] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/1_tutorial/1_sync.cpp] +[example_tutorial_sync] + +[endsect] + + + + +[section:tutorial_async Tutorial 2: going async with C++20 coroutines] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/1_tutorial/2_async.cpp] +[example_tutorial_async] + +[endsect] + + + + +[section:tutorial_with_params Tutorial 3: queries with parameters] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/1_tutorial/3_with_params.cpp] +[example_tutorial_with_params] + +[endsect] + + + + +[section:prepared_statements Prepared statements] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/prepared_statements.cpp] +[example_prepared_statements] + +[endsect] + + + + +[section:timeouts Setting timeouts to operations] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/timeouts.cpp] +[example_timeouts] + +[endsect] + + + + +[section:transactions Using transactions] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/transactions.cpp] +[example_transactions] + +[endsect] + + + + +[section:disable_tls Disabling TLS for a connection] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/disable_tls.cpp] +[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]. + +[import ../../example/2_simple/tls_certificate_verification.cpp] +[example_tls_certificate_verification] + +[endsect] + + + + +[section:metadata Metadata] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/metadata.cpp] +[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]. + +[import ../../example/2_simple/multi_function.cpp] +[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]. + +[import ../../example/2_simple/callbacks.cpp] +[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]. + +[import ../../example/2_simple/coroutines_cpp11.cpp] +[example_coroutines_cpp11] + +[endsect] + + + + +[section:unix_socket UNIX sockets] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/unix_socket.cpp] +[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]. + +[import ../../example/2_simple/batch_inserts.cpp] +[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]. + +[import ../../example/2_simple/batch_inserts_generic.cpp] +[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]. + +[import ../../example/2_simple/dynamic_filters.cpp] +[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]. + +[import ../../example/2_simple/patch_updates.cpp] +[example_patch_updates] + +[endsect] + + + + +[section:pipeline (Experimental) Pipelines] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/2_simple/pipeline.cpp] +[example_pipeline] + +[endsect] + + + + +[section:source_script Sourcing a .sql file using multi-queries] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/3_advanced/source_script.cpp] +[example_source_script] + +[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]. + +[import ../../example/3_advanced/connection_pool/main.cpp] +[example_connection_pool_main_cpp] + +[import ../../example/3_advanced/connection_pool/types.hpp] +[example_connection_pool_types_hpp] + +[import ../../example/3_advanced/connection_pool/repository.hpp] +[example_connection_pool_repository_hpp] + +[import ../../example/3_advanced/connection_pool/repository.cpp] +[example_connection_pool_repository_cpp] + +[import ../../example/3_advanced/connection_pool/handle_request.hpp] +[example_connection_pool_handle_request_hpp] + +[import ../../example/3_advanced/connection_pool/handle_request.cpp] +[example_connection_pool_handle_request_cpp] + +[import ../../example/3_advanced/connection_pool/server.hpp] +[example_connection_pool_server_hpp] + +[import ../../example/3_advanced/connection_pool/server.cpp] +[example_connection_pool_server_cpp] + +[import ../../example/3_advanced/connection_pool/log_error.hpp] +[example_connection_pool_log_error_hpp] + +[endsect] + + +[endsect] + diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py new file mode 100644 index 000000000..d82e12c89 --- /dev/null +++ b/tools/scripts/examples_qbk.py @@ -0,0 +1,186 @@ +#!/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 + +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 + + +def render_simple_cpp(id: str, path: str) -> str: + return f'[import ../../example/{path}]\n[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), p) for p in paths) + + +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: connections and sync functions'), + Example('tutorial_async', '1_tutorial/2_async.cpp', 'Tutorial 2: going async with C++20 coroutines'), + Example('tutorial_with_params', '1_tutorial/3_with_params.cpp', 'Tutorial 3: queries with parameters'), +] + +SIMPLE_EXAMPLES = [ + Example('prepared_statements', '2_simple/prepared_statements.cpp', 'Prepared statements'), + Example('timeouts', '2_simple/timeouts.cpp', 'Setting timeouts to operations'), + Example('transactions', '2_simple/transactions.cpp', 'Using 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('pipeline', '2_simple/pipeline.cpp', '(Experimental) Pipelines'), +] + +ADVANCED_EXAMPLES = [ + Example('source_script', '3_advanced/source_script.cpp', 'Sourcing a .sql file using multi-queries'), + 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') +] + +# [link mysql.examples.source_script Using multi-queries to source a .sql file] +# [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] ] + +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 main(): + # Render + 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, elm.path) + ) for elm in ALL_EXAMPLES) + ) + + # Write to output file + output_file = path.join(REPO_BASE, 'doc', 'qbk', '25_examples.qbk') + with open(output_file, 'wt') as f: + f.write(contents) + + +if __name__ == '__main__': + main() \ No newline at end of file From 5793277b31cd228fbbb5aa23ec31dcdd27b44bb4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 29 Oct 2024 20:58:17 +0100 Subject: [PATCH 29/77] Redo tutorial section --- doc/qbk/00_main.qbk | 14 ++-- doc/qbk/02_integrating.qbk | 2 +- doc/qbk/03_1_tutorial_sync.qbk | 123 +++++++++++++++++++++++++++++++++ doc/qbk/03_tutorial.qbk | 109 ----------------------------- example/1_tutorial/1_sync.cpp | 25 ++++--- example/1_tutorial/2_async.cpp | 13 ++-- 6 files changed, 151 insertions(+), 135 deletions(-) create mode 100644 doc/qbk/03_1_tutorial_sync.qbk delete mode 100644 doc/qbk/03_tutorial.qbk diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 37c046e46..646cf1d6e 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -56,8 +56,6 @@ [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].] [/ MySQL stuff] [def __Mysql__ [@https://www.mysql.com/ MySQL]] @@ -142,8 +140,8 @@ END [include 01_intro.qbk] [include 02_integrating.qbk] -[include 03_tutorial.qbk] -[include 04_overview.qbk] +[include 03_1_tutorial_sync.qbk] +[/ [include 04_overview.qbk] [include 05_dynamic_interface.qbk] [include 06_static_interface.qbk] [include 07_queries.qbk] @@ -163,11 +161,11 @@ END [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 24_pipeline.qbk] ] +[include 25_examples.qbk] -[section:ref Reference] +[/ [section:ref Reference] [xinclude helpers/quickref.xml] [block''''''] [include reference.qbk] @@ -182,4 +180,4 @@ END [include helpers/Stream.qbk] [include helpers/WritableFieldTuple.qbk] [block''''''] -[endsect] +[endsect] ] 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..010507b09 --- /dev/null +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -0,0 +1,123 @@ +[/ + 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!] + +[import ../../example/1_tutorial/1_sync.cpp] +[import ../../example/1_tutorial/2_async.cpp] +[import ../../example/1_tutorial/3_with_params.cpp] + +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] + +TODO: link to the next one. + +Full program listing for this tutorial is [link mysql.examples.tutorial_sync here]. + +[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/example/1_tutorial/1_sync.cpp b/example/1_tutorial/1_sync.cpp index aa21160cb..b808c954a 100644 --- a/example/1_tutorial/1_sync.cpp +++ b/example/1_tutorial/1_sync.cpp @@ -7,6 +7,13 @@ //[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 @@ -16,14 +23,10 @@ #include +//[tutorial_sync_namespaces namespace mysql = boost::mysql; namespace asio = boost::asio; - -/** - * This example uses the 'boost_mysql_examples' database, which you - * can get by running db_setup.sql. - * This example uses synchronous functions and handles errors using exceptions. - */ +//] void main_impl(int argc, char** argv) { @@ -33,7 +36,7 @@ void main_impl(int argc, char** argv) exit(1); } - //[tutorial_connection + //[tutorial_sync_connection // The execution context, required to run I/O operations. asio::io_context ctx; @@ -41,7 +44,7 @@ void main_impl(int argc, char** argv) mysql::any_connection conn(ctx); //] - //[tutorial_connect + //[tutorial_sync_connect // The hostname, username, password and database to use mysql::connect_params params; params.server_address.emplace_host_and_port(argv[3]); @@ -53,19 +56,19 @@ void main_impl(int argc, char** argv) conn.connect(params); //] - //[tutorial_query + //[tutorial_sync_query // Issue the SQL query to the server const char* sql = "SELECT 'Hello world!'"; 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(); //] diff --git a/example/1_tutorial/2_async.cpp b/example/1_tutorial/2_async.cpp index 3ec07f1f1..5932496a1 100644 --- a/example/1_tutorial/2_async.cpp +++ b/example/1_tutorial/2_async.cpp @@ -10,6 +10,11 @@ //[example_tutorial_async +/** + * This example is analogous to the synchronous tutorial, but uses async functions + * with C++20 coroutines, instead. + */ + #include #include #include @@ -27,12 +32,8 @@ namespace mysql = boost::mysql; namespace asio = boost::asio; /** - * This example is analogous to the synchronous tutorial, but uses async functions - * with C++20 coroutines, instead. It uses the 'boost_mysql_examples' database. - * You can get this database by running db_setup.sql. - * - * This function implements the main coroutine. - * It must have a return type of boost::asio::awaitable. + * 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 From 4bf2efab49d0475a9ebeaba8e218421ab6a18820 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 30 Oct 2024 14:34:55 +0100 Subject: [PATCH 30/77] tutorial async to discussion --- doc/qbk/00_main.qbk | 3 +- doc/qbk/03_1_tutorial_sync.qbk | 2 - doc/qbk/03_2_tutorial_async.qbk | 107 ++++++++++++++++++++++++++++++++ doc/qbk/25_examples.qbk | 12 ++-- example/1_tutorial/1_sync.cpp | 15 +++-- example/1_tutorial/2_async.cpp | 47 +++++++++----- tools/scripts/examples_qbk.py | 6 +- 7 files changed, 159 insertions(+), 33 deletions(-) create mode 100644 doc/qbk/03_2_tutorial_async.qbk diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 646cf1d6e..601ff74c1 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 @@ -141,6 +141,7 @@ END [include 01_intro.qbk] [include 02_integrating.qbk] [include 03_1_tutorial_sync.qbk] +[include 03_2_tutorial_async.qbk] [/ [include 04_overview.qbk] [include 05_dynamic_interface.qbk] [include 06_static_interface.qbk] diff --git a/doc/qbk/03_1_tutorial_sync.qbk b/doc/qbk/03_1_tutorial_sync.qbk index 010507b09..2d6fa4045 100644 --- a/doc/qbk/03_1_tutorial_sync.qbk +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -8,8 +8,6 @@ [section:tutorial_sync Tutorial 1: hello world!] [import ../../example/1_tutorial/1_sync.cpp] -[import ../../example/1_tutorial/2_async.cpp] -[import ../../example/1_tutorial/3_with_params.cpp] In this first tutorial, we will write a simple program to demonstrate the basic concepts. We will connect to the server diff --git a/doc/qbk/03_2_tutorial_async.qbk b/doc/qbk/03_2_tutorial_async.qbk new file mode 100644 index 000000000..c695a9ace --- /dev/null +++ b/doc/qbk/03_2_tutorial_async.qbk @@ -0,0 +1,107 @@ +[/ + 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] + +[import ../../example/1_tutorial/1_sync.cpp] +[import ../../example/1_tutorial/2_async.cpp] + +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. +There are ways to prevent this (TODO: link to it) + + + +[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] + +TODO: link to the next one. + +Full program listing for this tutorial is [link mysql.examples.tutorial_async here]. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/25_examples.qbk b/doc/qbk/25_examples.qbk index 5510c473c..5d5ed8772 100644 --- a/doc/qbk/25_examples.qbk +++ b/doc/qbk/25_examples.qbk @@ -17,9 +17,9 @@ Here is a list of available examples: Self-contained programs demonstrating the basic concepts. -* [link mysql.examples.tutorial_sync Tutorial 1: connections and sync functions] -* [link mysql.examples.tutorial_async Tutorial 2: going async with C++20 coroutines] -* [link mysql.examples.tutorial_with_params Tutorial 3: queries with parameters] +* [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] [heading Simple programs] @@ -73,7 +73,7 @@ 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: connections and sync functions] +[section:tutorial_sync Tutorial 1 listing: hello world!] This example assumes you have gone through the [link mysql.examples.setup setup]. @@ -85,7 +85,7 @@ This example assumes you have gone through the [link mysql.examples.setup setup] -[section:tutorial_async Tutorial 2: going async with C++20 coroutines] +[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]. @@ -97,7 +97,7 @@ This example assumes you have gone through the [link mysql.examples.setup setup] -[section:tutorial_with_params Tutorial 3: queries with parameters] +[section:tutorial_with_params Tutorial 3 listing: queries with parameters] This example assumes you have gone through the [link mysql.examples.setup setup]. diff --git a/example/1_tutorial/1_sync.cpp b/example/1_tutorial/1_sync.cpp index b808c954a..19e13d33a 100644 --- a/example/1_tutorial/1_sync.cpp +++ b/example/1_tutorial/1_sync.cpp @@ -36,6 +36,10 @@ void main_impl(int argc, char** argv) exit(1); } + const char* hostname = argv[3]; + const char* username = argv[1]; + const char* password = argv[2]; + //[tutorial_sync_connection // The execution context, required to run I/O operations. asio::io_context ctx; @@ -44,13 +48,13 @@ void main_impl(int argc, char** argv) mysql::any_connection conn(ctx); //] + //[tutorial_sync_main //[tutorial_sync_connect - // The hostname, username, password and database to use + // The hostname, username and password to use 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"; + params.server_address.emplace_host_and_port(hostname); + params.username = username; + params.password = password; // Connect to the server conn.connect(params); @@ -72,6 +76,7 @@ void main_impl(int argc, char** argv) // Close the connection conn.close(); //] + //] } int main(int argc, char** argv) diff --git a/example/1_tutorial/2_async.cpp b/example/1_tutorial/2_async.cpp index 5932496a1..a9b06c89c 100644 --- a/example/1_tutorial/2_async.cpp +++ b/example/1_tutorial/2_async.cpp @@ -42,20 +42,20 @@ namespace asio = boost::asio; * 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. */ -asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) +//[tutorial_async_coro +asio::awaitable coro_main( + mysql::any_connection& conn, + std::string_view server_hostname, + std::string_view username, + std::string_view password +) { - // 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. // TLS is used by default. - mysql::connect_params params{ - .server_address = mysql::host_and_port{std::move(server_hostname)}, - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); @@ -71,6 +71,7 @@ asio::awaitable coro_main(std::string server_hostname, std::string usernam // Close the connection co_await conn.async_close(); } +//] void main_impl(int argc, char** argv) { @@ -80,16 +81,27 @@ void main_impl(int argc, char** argv) exit(1); } + //[tutorial_async_connection // The execution context, required to run I/O operations. asio::io_context ctx; - // The entry point. We pass in a function returning - // boost::asio::awaitable, as required. - // Calling co_spawn enqueues the coroutine for execution. + // 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, - [argv] { return coro_main(argv[3], argv[1], argv[2]); }, - // If any exception is thrown in the coroutine body, rethrow it. + + // 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) { @@ -97,9 +109,12 @@ void main_impl(int argc, char** argv) } } ); + //] + //[tutorial_async_run // Calling run will actually execute the coroutine until completion ctx.run(); + //] } int main(int argc, char** argv) diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index d82e12c89..5ead8dacf 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -115,9 +115,9 @@ def get_file_id(p: str): # List all examples here TUTORIALS = [ - Example('tutorial_sync', '1_tutorial/1_sync.cpp', 'Tutorial 1: connections and sync functions'), - Example('tutorial_async', '1_tutorial/2_async.cpp', 'Tutorial 2: going async with C++20 coroutines'), - Example('tutorial_with_params', '1_tutorial/3_with_params.cpp', 'Tutorial 3: queries with parameters'), + 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'), ] SIMPLE_EXAMPLES = [ From d8aae222f57de3dccffb3f453c2e4d583ba47343 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 30 Oct 2024 18:08:19 +0100 Subject: [PATCH 31/77] Tutorial 3 outline --- doc/qbk/00_main.qbk | 1 + doc/qbk/03_3_tutorial_with_params.qbk | 121 ++++++++++++++++++++++++++ example/1_tutorial/3_with_params.cpp | 56 ++++++++---- 3 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 doc/qbk/03_3_tutorial_with_params.qbk diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 601ff74c1..620610426 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -142,6 +142,7 @@ END [include 02_integrating.qbk] [include 03_1_tutorial_sync.qbk] [include 03_2_tutorial_async.qbk] +[include 03_3_tutorial_with_params.qbk] [/ [include 04_overview.qbk] [include 05_dynamic_interface.qbk] [include 06_static_interface.qbk] 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..cbe3f690f --- /dev/null +++ b/doc/qbk/03_3_tutorial_with_params.qbk @@ -0,0 +1,121 @@ +[/ + 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 3: queries with parameters] + +[import ../../example/1_tutorial/3_with_params.cpp] + +We've been running simple text queries, but real world often involves +running queries containing user-supplied parameters. + +Consider an employee database. Each employee is identified by a unique +numeric ID. Table may be defined as follows: + +CREATE TABLE ... + +We will write a program that retrieves an employee by ID from the database and prints their name. +This implies running a query like: + +SELECT ... FROM employee WHERE id = + +Where must be replaced by the value the user supplies. +We will read it from the command line args for simplicity, but +you may read it from a file or an HTTP request. + + + + +[heading Available tools] + +We don't control , so we must consider it untrusted. +We must never use raw string concatenation to build our query. +Otherwise, malicious values can cause SQL injection vulnerabilities, +which are extremely severe. + +Two available options: + +* Compose the query dynamically in the client, using specialized tools + to avoid SQL injection. TODO: link to details +* Perform parameter substitution server side using prepared statements. + TODO: link to details + +The first option is adequate for one-off or light queries like ours. +Prepared statements are more adequate for queries returning a lot of +numeric data or when running a query several times. +In this tutorial, we will use client-side generation. + + + + +[heading Using with_params] + +The same [refmem any_connection::async_execute] can expand and +execute queries with parameters, too. We will replace the string +literal by a [reflink with_params] expression: + +[tutorial_with_params_execute] + +When `async_execute` encounters a `with_params_t` object, it does +the following: + +# It expands the given query string, with the passed parameters. + The syntax is akin to `std::format`. +# It executes the resulting string, as if a string literal was passed. + +This is all we need. `with_params` supports formatting numbers, strings, +dates, times and many other types. `with_params` and related functions +form what we call "client-side SQL formatting" - TODO: link. + + + +[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. +To create a connection, we need a reference to the `io_context` we want +to use. We could pass it as parameter, but we don't need it - coroutines +already know where they are executing. We can write the following: + +[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 I/O. + It only retrieves the current coroutine's executor. +] + + + + +[heading Connecting with database] + +To run the above code, we need to select a database where the `employee` +table exists. If you followed the setup instructions (TODO?), the table +lives in the "boost_mysql_examples" database. We can use +[refmem connect_params database] to select a database when connecting: + +[tutorial_with_params_connect_params] + + + + +[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]. + + +TODO: link to the next one. + + +[endsect] \ No newline at end of file diff --git a/example/1_tutorial/3_with_params.cpp b/example/1_tutorial/3_with_params.cpp index 3ad2c7bd0..75d3e1147 100644 --- a/example/1_tutorial/3_with_params.cpp +++ b/example/1_tutorial/3_with_params.cpp @@ -10,6 +10,15 @@ //[example_tutorial_with_params +/** + * This example shows how to issue queries with parameters containing + * untrusted input securely. Given an employee ID, it prints their first 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 @@ -29,42 +38,46 @@ namespace mysql = boost::mysql; namespace asio = boost::asio; -/** - * This example shows how to issue queries with parameters containing - * untrusted input securely. Given an employee ID, it prints their first name. - * The example builds on the previous async tutorial. - */ +//[tutorial_with_params_coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::int64_t employee_id ) { + //[tutorial_with_params_connection // 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); + //] + //[tutorial_with_params_connect_params // The hostname, username, password and database to use. - mysql::connect_params params{ - .server_address = mysql::host_and_port{std::move(server_hostname)}, - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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 FROM employee WHERE id = {}", employee_id), + mysql::with_params( + "SELECT CONCAT(first_name, ' ', last_name) FROM employee WHERE id = {}", + employee_id + ), result ); + //] // Did we find an employee with that ID? if (result.rows().empty()) @@ -80,6 +93,7 @@ asio::awaitable coro_main( // Close the connection co_await conn.async_close(); } +//] void main_impl(int argc, char** argv) { @@ -92,13 +106,17 @@ void main_impl(int argc, char** argv) // The execution context, required to run I/O operations. asio::io_context ctx; - // The entry point. We pass in a function returning - // boost::asio::awaitable, as required. - // Calling co_spawn enqueues the coroutine for execution. + // 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])); }, - // If any exception is thrown in the coroutine body, rethrow it. + + // 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) { From 3b379da48c113b9720655aff8ed13a530d40e0f1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 30 Oct 2024 19:01:44 +0100 Subject: [PATCH 32/77] Tutorial 3 final writing --- doc/qbk/03_3_tutorial_with_params.qbk | 129 ++++++++++++++++---------- example/1_tutorial/3_with_params.cpp | 15 +-- 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/doc/qbk/03_3_tutorial_with_params.qbk b/doc/qbk/03_3_tutorial_with_params.qbk index cbe3f690f..83d857205 100644 --- a/doc/qbk/03_3_tutorial_with_params.qbk +++ b/doc/qbk/03_3_tutorial_with_params.qbk @@ -9,66 +9,107 @@ [import ../../example/1_tutorial/3_with_params.cpp] -We've been running simple text queries, but real world often involves +Until now, our SQL queries were hard-coded string literals. +However, most real-world use cases involve running queries containing user-supplied parameters. -Consider an employee database. Each employee is identified by a unique -numeric ID. Table may be defined as follows: +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. -CREATE TABLE ... +The employee table is defined as: -We will write a program that retrieves an employee by ID from the database and prints their name. -This implies running a query like: +[!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 + ); +``` -SELECT ... FROM employee WHERE id = +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. -Where must be replaced by the value the user supplies. -We will read it from the command line args for simplicity, but -you may read it from a file or an HTTP request. +[heading Avoiding SQL injection] -[heading Available tools] +We need to build a query like the following: -We don't control , so we must consider it untrusted. -We must never use raw string concatenation to build our query. +[!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. -Two available options: +Boost.MySQL offers two options to deal with this: -* Compose the query dynamically in the client, using specialized tools - to avoid SQL injection. TODO: link to details -* Perform parameter substitution server side using prepared statements. - TODO: link to details +# 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. -The first option is adequate for one-off or light queries like ours. -Prepared statements are more adequate for queries returning a lot of -numeric data or when running a query several times. -In this tutorial, we will use client-side generation. +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] -The same [refmem any_connection::async_execute] can expand and -execute queries with parameters, too. We will replace the string -literal by a [reflink with_params] expression: +[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] -When `async_execute` encounters a `with_params_t` object, it does -the following: +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] -# It expands the given query string, with the passed parameters. - The syntax is akin to `std::format`. -# It executes the resulting string, as if a string literal was passed. -This is all we need. `with_params` supports formatting numbers, strings, -dates, times and many other types. `with_params` and related functions -form what we call "client-side SQL formatting" - TODO: link. @@ -76,35 +117,25 @@ form what we call "client-side SQL formatting" - TODO: link. 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. -To create a connection, we need a reference to the `io_context` we want -to use. We could pass it as parameter, but we don't need it - coroutines -already know where they are executing. We can write the following: +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 +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 I/O. + `co_await asio::this_coro::executor` does not perform any I/O. It only retrieves the current coroutine's executor. ] -[heading Connecting with database] - -To run the above code, we need to select a database where the `employee` -table exists. If you followed the setup instructions (TODO?), the table -lives in the "boost_mysql_examples" database. We can use -[refmem connect_params database] to select a database when connecting: - -[tutorial_with_params_connect_params] - - - [heading Wrapping up] diff --git a/example/1_tutorial/3_with_params.cpp b/example/1_tutorial/3_with_params.cpp index 75d3e1147..1583140b4 100644 --- a/example/1_tutorial/3_with_params.cpp +++ b/example/1_tutorial/3_with_params.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -47,7 +48,6 @@ asio::awaitable coro_main( ) { //[tutorial_with_params_connection - // 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); //] @@ -71,14 +71,12 @@ asio::awaitable coro_main( // SQL injection. mysql::results result; co_await conn.async_execute( - mysql::with_params( - "SELECT CONCAT(first_name, ' ', last_name) FROM employee WHERE id = {}", - employee_id - ), + 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()) { @@ -86,9 +84,12 @@ asio::awaitable coro_main( } else { - // Print the first field in the first row - std::cout << "Employee's name is: " << result.rows().at(0).at(0) << std::endl; + // 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(); From 6336f55edc0cc8695a718b96def73724ed867189 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 30 Oct 2024 19:38:39 +0100 Subject: [PATCH 33/77] Clarify affected_rows --- include/boost/mysql/execution_state.hpp | 3 +++ include/boost/mysql/results.hpp | 3 +++ include/boost/mysql/static_execution_state.hpp | 3 +++ include/boost/mysql/static_results.hpp | 3 +++ 4 files changed, 12 insertions(+) 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. From 59c2433e90e69920cda830874e1d26024cf5cf67 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 2 Nov 2024 12:48:02 +0100 Subject: [PATCH 34/77] Static iface tutorial --- example/1_tutorial/3_with_params.cpp | 2 +- example/1_tutorial/4_static_interface.cpp | 185 ++++++++++++++++++++++ example/CMakeLists.txt | 7 +- 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 example/1_tutorial/4_static_interface.cpp diff --git a/example/1_tutorial/3_with_params.cpp b/example/1_tutorial/3_with_params.cpp index 1583140b4..7fd158c95 100644 --- a/example/1_tutorial/3_with_params.cpp +++ b/example/1_tutorial/3_with_params.cpp @@ -12,7 +12,7 @@ /** * This example shows how to issue queries with parameters containing - * untrusted input securely. Given an employee ID, it prints their first name. + * 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 diff --git a/example/1_tutorial/4_static_interface.cpp b/example/1_tutorial/4_static_interface.cpp new file mode 100644 index 000000000..dcb5d0c40 --- /dev/null +++ b/example/1_tutorial/4_static_interface.cpp @@ -0,0 +1,185 @@ +// +// 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_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. +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. The first field is the first name, + // and the second, the last name. + 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/CMakeLists.txt b/example/CMakeLists.txt index 2e31d9977..097b9f216 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -67,9 +67,10 @@ endfunction() set(REGULAR_ARGS example_user example_password ${SERVER_HOST}) # Tutorials -add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_with_params 1_tutorial/3_with_params.cpp ARGS ${REGULAR_ARGS} 1) +add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) +add_example(tutorial_with_params 1_tutorial/3_with_params.cpp ARGS ${REGULAR_ARGS} 1) +add_example(tutorial_static_interface 1_tutorial/4_static_interface.cpp ARGS ${REGULAR_ARGS} 1 LIBS Boost::pfr) # Simple add_simple_example(callbacks ARGS ${REGULAR_ARGS}) From 7d901d6d9ab94d0910072dbc3eb9267a3ddb0739 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 2 Nov 2024 12:56:39 +0100 Subject: [PATCH 35/77] snippets_fixture --- .../snippets/snippets_fixture.hpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/integration/include/test_integration/snippets/snippets_fixture.hpp 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..6b7891b4d --- /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_FIXTURE_HPP +#define BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_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 From f3be3c0150be56db8fd7c0e7e9eaf034d7618661 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 2 Nov 2024 13:55:14 +0100 Subject: [PATCH 36/77] Add examples_qbk to file_header --- tools/scripts/file_headers.py | 7 +++++++ 1 file changed, 7 insertions(+) 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__': From 507e18f91301a939b0474cdea2c3df87eba3089a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 2 Nov 2024 13:56:48 +0100 Subject: [PATCH 37/77] Tutorial 4 discussion --- doc/qbk/00_main.qbk | 8 ++ doc/qbk/03_1_tutorial_sync.qbk | 2 - doc/qbk/03_2_tutorial_async.qbk | 3 - doc/qbk/03_3_tutorial_with_params.qbk | 2 - doc/qbk/03_4_tutorial_static_interface.qbk | 108 ++++++++++++++++++ doc/qbk/06_static_interface.qbk | 6 +- example/1_tutorial/4_static_interface.cpp | 4 +- test/integration/CMakeLists.txt | 1 + test/integration/Jamfile | 1 + .../snippets/snippets_fixture.hpp | 4 +- test/integration/test/snippets/tutorials.cpp | 39 +++++++ 11 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 doc/qbk/03_4_tutorial_static_interface.qbk create mode 100644 test/integration/test/snippets/tutorials.cpp diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 620610426..2c01ada6a 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -56,6 +56,8 @@ [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 __Describe__ [@boost:/libs/describe/index.html Boost.Describe]] +[def __Pfr__ [@boost:/libs/pfr/index.html Boost.Pfr]] [/ MySQL stuff] [def __Mysql__ [@https://www.mysql.com/ MySQL]] @@ -122,7 +124,12 @@ BEGIN END ```] +[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 ../../test/integration/include/test_integration/snippets/describe.hpp] +[import ../../test/integration/test/snippets/tutorials.cpp] [import ../../test/integration/test/snippets/overview.cpp] [import ../../test/integration/test/snippets/dynamic.cpp] [import ../../test/integration/test/snippets/static.cpp] @@ -143,6 +150,7 @@ END [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] diff --git a/doc/qbk/03_1_tutorial_sync.qbk b/doc/qbk/03_1_tutorial_sync.qbk index 2d6fa4045..9fdbdbb74 100644 --- a/doc/qbk/03_1_tutorial_sync.qbk +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -7,8 +7,6 @@ [section:tutorial_sync Tutorial 1: hello world!] -[import ../../example/1_tutorial/1_sync.cpp] - 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!"`. diff --git a/doc/qbk/03_2_tutorial_async.qbk b/doc/qbk/03_2_tutorial_async.qbk index c695a9ace..accae63a3 100644 --- a/doc/qbk/03_2_tutorial_async.qbk +++ b/doc/qbk/03_2_tutorial_async.qbk @@ -7,9 +7,6 @@ [section:tutorial_async Tutorial 2: going async with C++20 coroutines] -[import ../../example/1_tutorial/1_sync.cpp] -[import ../../example/1_tutorial/2_async.cpp] - In the [link mysql.tutorial_sync previous tutorial] we used synchronous functions. They are simple, but have a number of limitations: diff --git a/doc/qbk/03_3_tutorial_with_params.qbk b/doc/qbk/03_3_tutorial_with_params.qbk index 83d857205..fdc9fb2e5 100644 --- a/doc/qbk/03_3_tutorial_with_params.qbk +++ b/doc/qbk/03_3_tutorial_with_params.qbk @@ -7,8 +7,6 @@ [section:tutorial_async Tutorial 3: queries with parameters] -[import ../../example/1_tutorial/3_with_params.cpp] - Until now, our SQL queries were hard-coded string literals. However, most real-world use cases involve running queries containing user-supplied parameters. 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..6235871e1 --- /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_async 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. +See (TODO: link to discussion or example) +for sample code. + + +[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]. + + +TODO: explain that this is over and point to other resources. + +[endsect] \ No newline at end of file diff --git a/doc/qbk/06_static_interface.qbk b/doc/qbk/06_static_interface.qbk index b329d5252..882033898 100644 --- a/doc/qbk/06_static_interface.qbk +++ b/doc/qbk/06_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/example/1_tutorial/4_static_interface.cpp b/example/1_tutorial/4_static_interface.cpp index dcb5d0c40..1f18d6911 100644 --- a/example/1_tutorial/4_static_interface.cpp +++ b/example/1_tutorial/4_static_interface.cpp @@ -50,6 +50,7 @@ void print_employee(std::string_view first_name, std::string_view last_name) //[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; @@ -102,8 +103,7 @@ asio::awaitable coro_main( } else { - // Print the retrieved details. The first field is the first name, - // and the second, the last name. + // Print the retrieved details const employee& emp = result.rows()[0]; print_employee(emp.first_name, emp.last_name); } diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index d1f2b2695..aa4ce09f8 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable( test/database_types.cpp # Snippets + test/snippets/tutorials.cpp test/snippets/overview.cpp test/snippets/dynamic.cpp test/snippets/static.cpp diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 341da1dd9..06d7e18bc 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -44,6 +44,7 @@ run test/database_types.cpp # Snippets + test/snippets/tutorials.cpp test/snippets/overview.cpp test/snippets/dynamic.cpp test/snippets/static.cpp diff --git a/test/integration/include/test_integration/snippets/snippets_fixture.hpp b/test/integration/include/test_integration/snippets/snippets_fixture.hpp index 6b7891b4d..43e9787d2 100644 --- a/test/integration/include/test_integration/snippets/snippets_fixture.hpp +++ b/test/integration/include/test_integration/snippets/snippets_fixture.hpp @@ -5,8 +5,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_FIXTURE_HPP -#define BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_SNIPPETS_FIXTURE_HPP +#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 diff --git a/test/integration/test/snippets/tutorials.cpp b/test/integration/test/snippets/tutorials.cpp new file mode 100644 index 000000000..b5a678e6d --- /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 = 0", 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 From bf07836228fca0c5ede93b786183b1f86842c2f4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 2 Nov 2024 14:06:11 +0100 Subject: [PATCH 38/77] Add tutorial 4 to examples section --- doc/qbk/25_examples.qbk | 13 +++++++++++++ tools/scripts/examples_qbk.py | 1 + 2 files changed, 14 insertions(+) diff --git a/doc/qbk/25_examples.qbk b/doc/qbk/25_examples.qbk index 5d5ed8772..f9b0a66fb 100644 --- a/doc/qbk/25_examples.qbk +++ b/doc/qbk/25_examples.qbk @@ -20,6 +20,7 @@ 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] @@ -109,6 +110,18 @@ This example assumes you have gone through the [link mysql.examples.setup setup] +[section:tutorial_static_interface Tutorial 4 listing: the static interface] + +This example assumes you have gone through the [link mysql.examples.setup setup]. + +[import ../../example/1_tutorial/4_static_interface.cpp] +[example_tutorial_static_interface] + +[endsect] + + + + [section:prepared_statements Prepared statements] This example assumes you have gone through the [link mysql.examples.setup setup]. diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index 5ead8dacf..8a99a3acb 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -118,6 +118,7 @@ def get_file_id(p: str): 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 = [ From cd3bfbc62d9e8c43564d7df86064f6ae53febabf Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 3 Nov 2024 21:03:48 +0100 Subject: [PATCH 39/77] WIP connection establishment --- doc/qbk/00_main.qbk | 9 +- doc/qbk/05_connection_establishment.qbk | 281 ++++++++++++++++++ doc/qbk/13_ssl.qbk | 91 ------ doc/qbk/14_other_streams.qbk | 84 ------ doc/qbk/16_connparams.qbk | 104 ------- doc/qbk/20_any_connection.qbk | 83 ------ doc/qbk/TODO_templated_connection.qbk | 187 ++++++++++++ example/2_simple/disable_tls.cpp | 24 +- .../2_simple/tls_certificate_verification.cpp | 37 ++- example/2_simple/unix_socket.cpp | 25 +- test/integration/CMakeLists.txt | 1 + test/integration/Jamfile | 1 + .../snippets/connection_establishment.cpp | 40 +++ 13 files changed, 563 insertions(+), 404 deletions(-) create mode 100644 doc/qbk/05_connection_establishment.qbk delete mode 100644 doc/qbk/13_ssl.qbk delete mode 100644 doc/qbk/14_other_streams.qbk delete mode 100644 doc/qbk/16_connparams.qbk delete mode 100644 doc/qbk/20_any_connection.qbk create mode 100644 doc/qbk/TODO_templated_connection.qbk create mode 100644 test/integration/test/snippets/connection_establishment.cpp diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 2c01ada6a..ac92f93de 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -58,6 +58,7 @@ [def __boost_optional__ [@boost:/libs/optional/index.html `boost::optional`]] [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]] @@ -151,8 +152,9 @@ END [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 04_overview.qbk] ] +[include 05_connection_establishment.qbk] +[/ [include 05_dynamic_interface.qbk] [include 06_static_interface.qbk] [include 07_queries.qbk] [include 08_prepared_statements.qbk] @@ -160,14 +162,11 @@ END [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] diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk new file mode 100644 index 000000000..579a8d3ec --- /dev/null +++ b/doc/qbk/05_connection_establishment.qbk @@ -0,0 +1,281 @@ +[/ + 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_validation 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 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 +[refmem 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] + + + +[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. +] + + +[section Closing a connection] + +[endsect] + + + + + + +[section 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. + +[endsect] + + +destroying a connection does not leak any resources +but calling async_close ensures a clean closing. This includes notifying the server, +so it closes the connection immediately, and cleanly shutting down TLS. + +[endsect] [/ connparams] 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/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/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/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/TODO_templated_connection.qbk b/doc/qbk/TODO_templated_connection.qbk new file mode 100644 index 000000000..a0803f025 --- /dev/null +++ b/doc/qbk/TODO_templated_connection.qbk @@ -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) +] + +[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. + +Current status: `connection` and aliases are still maintained +(not deprecated), but we don't recommend using them 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. +That is, the transport can be configured. + +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. + ] + ] +] + +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__: + +[overview_connection] TODO: adapt + +In contrast, when using [reflink any_connection], passing a +__ssl_context__ is not mandatory. +One will be created for you if you didn't pass one and the connection +uses TLS. + + + + +[heading Connection establishment and termination] + +* 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. + + +[endsect] diff --git a/example/2_simple/disable_tls.cpp b/example/2_simple/disable_tls.cpp index 57fcc2248..87fbf3049 100644 --- a/example/2_simple/disable_tls.cpp +++ b/example/2_simple/disable_tls.cpp @@ -32,26 +32,32 @@ #include #include +#include namespace mysql = boost::mysql; namespace asio = boost::asio; // The main coroutine -asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) +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); - // The socket path, username, password and database to use. + //[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{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples", - .ssl = mysql::ssl_mode::disable, - }; + 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); diff --git a/example/2_simple/tls_certificate_verification.cpp b/example/2_simple/tls_certificate_verification.cpp index 3a4ad929f..13b9488f4 100644 --- a/example/2_simple/tls_certificate_verification.cpp +++ b/example/2_simple/tls_certificate_verification.cpp @@ -22,8 +22,6 @@ * can get by running db_setup.sql. * Additionally, your server must be configured with a trusted certificate * with a common name of "mysql". - * - * TODO: this should probably be setting TLS SNI */ #include @@ -69,8 +67,13 @@ OzBrmpfHEhF6NDU= )%"; // The main coroutine -asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) +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); @@ -86,31 +89,27 @@ asio::awaitable coro_main(std::string server_hostname, std::string usernam // 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". + // We expect the server certificate's common name to be equal to the configured hostname. // If it's not, the certificate will be rejected and handshake or connect will fail. - // Replace "mysql" by the hostname you expect in your certificate's common name - // TODO: we could set this to server_hostname and disable the test in some systems - ssl_ctx.set_verify_callback(asio::ssl::host_name_verification("mysql")); + ssl_ctx.set_verify_callback(asio::ssl::host_name_verification(std::string(server_hostname))); // 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_context = &ssl_ctx} - ); + 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{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; - - // Connect to the server + 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; diff --git a/example/2_simple/unix_socket.cpp b/example/2_simple/unix_socket.cpp index 3cdd66c93..26f502e6f 100644 --- a/example/2_simple/unix_socket.cpp +++ b/example/2_simple/unix_socket.cpp @@ -29,29 +29,36 @@ #include #include +#include namespace mysql = boost::mysql; namespace asio = boost::asio; // The main coroutine -asio::awaitable coro_main(std::string unix_socket_path, std::string username, std::string password) +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 - // Instead of a hostname and port, we use a unix_path as server_address. + // 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{ - .server_address = mysql::unix_path(std::move(unix_socket_path)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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; diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index aa4ce09f8..c3c6ad176 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable( # 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 diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 06d7e18bc..47cf884f0 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -46,6 +46,7 @@ run # 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 diff --git a/test/integration/test/snippets/connection_establishment.cpp b/test/integration/test/snippets/connection_establishment.cpp new file mode 100644 index 000000000..e13ecad90 --- /dev/null +++ b/test/integration/test/snippets/connection_establishment.cpp @@ -0,0 +1,40 @@ +// +// 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_common/io_context_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 + //] + } +} + +} // namespace From 4d61102597c7786656cb177eb23669942d335085 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 11:20:21 +0100 Subject: [PATCH 40/77] Change import autogeneration --- doc/qbk/00_main.qbk | 50 +++++++++++++++++----- doc/qbk/25_examples.qbk | 29 ------------- tools/scripts/examples_qbk.py | 80 ++++++++++++++++++++++++----------- 3 files changed, 95 insertions(+), 64 deletions(-) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index ac92f93de..53b873841 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -125,26 +125,54 @@ BEGIN END ```] +[/ 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 ../../test/integration/include/test_integration/snippets/describe.hpp] -[import ../../test/integration/test/snippets/tutorials.cpp] -[import ../../test/integration/test/snippets/overview.cpp] -[import ../../test/integration/test/snippets/dynamic.cpp] -[import ../../test/integration/test/snippets/static.cpp] +[import ../../example/2_simple/prepared_statements.cpp] +[import ../../example/2_simple/timeouts.cpp] +[import ../../example/2_simple/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/pipeline.cpp] +[import ../../example/3_advanced/source_script.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/multi_function.cpp] -[import ../../test/integration/test/snippets/metadata.cpp] +[import ../../test/integration/test/snippets/connection_establishment.cpp] [import ../../test/integration/test/snippets/charsets.cpp] -[import ../../test/integration/test/snippets/time_types.cpp] +[import ../../test/integration/test/snippets/sql_formatting_custom.cpp] +[import ../../test/integration/test/snippets/multi_function.cpp] [import ../../test/integration/test/snippets/any_connection.cpp] +[import ../../test/integration/test/snippets/tutorials.cpp] +[import ../../test/integration/test/snippets/metadata.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] diff --git a/doc/qbk/25_examples.qbk b/doc/qbk/25_examples.qbk index f9b0a66fb..d63644142 100644 --- a/doc/qbk/25_examples.qbk +++ b/doc/qbk/25_examples.qbk @@ -78,7 +78,6 @@ The root MySQL user for these containers is `root` and has an empty password. This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/1_tutorial/1_sync.cpp] [example_tutorial_sync] [endsect] @@ -90,7 +89,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/1_tutorial/2_async.cpp] [example_tutorial_async] [endsect] @@ -102,7 +100,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/1_tutorial/3_with_params.cpp] [example_tutorial_with_params] [endsect] @@ -114,7 +111,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/1_tutorial/4_static_interface.cpp] [example_tutorial_static_interface] [endsect] @@ -126,7 +122,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/prepared_statements.cpp] [example_prepared_statements] [endsect] @@ -138,7 +133,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/timeouts.cpp] [example_timeouts] [endsect] @@ -150,7 +144,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/transactions.cpp] [example_transactions] [endsect] @@ -162,7 +155,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/disable_tls.cpp] [example_disable_tls] [endsect] @@ -174,7 +166,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/tls_certificate_verification.cpp] [example_tls_certificate_verification] [endsect] @@ -186,7 +177,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/metadata.cpp] [example_metadata] [endsect] @@ -198,7 +188,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/multi_function.cpp] [example_multi_function] [endsect] @@ -210,7 +199,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/callbacks.cpp] [example_callbacks] [endsect] @@ -222,7 +210,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/coroutines_cpp11.cpp] [example_coroutines_cpp11] [endsect] @@ -234,7 +221,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/unix_socket.cpp] [example_unix_socket] [endsect] @@ -246,7 +232,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/batch_inserts.cpp] [example_batch_inserts] [endsect] @@ -258,7 +243,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/batch_inserts_generic.cpp] [example_batch_inserts_generic] [endsect] @@ -270,7 +254,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/dynamic_filters.cpp] [example_dynamic_filters] [endsect] @@ -282,7 +265,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/patch_updates.cpp] [example_patch_updates] [endsect] @@ -294,7 +276,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/2_simple/pipeline.cpp] [example_pipeline] [endsect] @@ -306,7 +287,6 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/3_advanced/source_script.cpp] [example_source_script] [endsect] @@ -318,31 +298,22 @@ This example assumes you have gone through the [link mysql.examples.setup setup] This example assumes you have gone through the [link mysql.examples.setup setup]. -[import ../../example/3_advanced/connection_pool/main.cpp] [example_connection_pool_main_cpp] -[import ../../example/3_advanced/connection_pool/types.hpp] [example_connection_pool_types_hpp] -[import ../../example/3_advanced/connection_pool/repository.hpp] [example_connection_pool_repository_hpp] -[import ../../example/3_advanced/connection_pool/repository.cpp] [example_connection_pool_repository_cpp] -[import ../../example/3_advanced/connection_pool/handle_request.hpp] [example_connection_pool_handle_request_hpp] -[import ../../example/3_advanced/connection_pool/handle_request.cpp] [example_connection_pool_handle_request_cpp] -[import ../../example/3_advanced/connection_pool/server.hpp] [example_connection_pool_server_hpp] -[import ../../example/3_advanced/connection_pool/server.cpp] [example_connection_pool_server_cpp] -[import ../../example/3_advanced/connection_pool/log_error.hpp] [example_connection_pool_log_error_hpp] [endsect] diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index 8a99a3acb..1b1822276 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -7,7 +7,7 @@ # from typing import NamedTuple, List -from os import path +from os import path, listdir REPO_BASE = path.abspath(path.join(path.dirname(path.realpath(__file__)), '..', '..')) @@ -23,19 +23,6 @@ class MultiExample(NamedTuple): title: str -def render_simple_cpp(id: str, path: str) -> str: - return f'[import ../../example/{path}]\n[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), p) for p in paths) - - LINK_TEMPLATE = '* [link mysql.examples.{example.id} {example.title}]' SECTION_TEMPLATE = ''' [section:{example.id} {example.title}] @@ -111,8 +98,6 @@ def get_file_id(p: str): ''' - - # List all examples here TUTORIALS = [ Example('tutorial_sync', '1_tutorial/1_sync.cpp', 'Tutorial 1 listing: hello world!'), @@ -160,27 +145,74 @@ def get_file_id(p: str): ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES -def render_links(examples: List[Example]) -> str: +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 - contents = TEMPLATE.format( - tutorial_links=render_links(TUTORIALS), - simple_links=render_links(SIMPLE_EXAMPLES), + 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, elm.path) + 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 - output_file = path.join(REPO_BASE, 'doc', 'qbk', '25_examples.qbk') - with open(output_file, 'wt') as f: - f.write(contents) + _replace_imports(import_contents) + _write_file(['doc', 'qbk', '25_examples.qbk'], example_contents) if __name__ == '__main__': From 8fa9371e1614a98880135b3f219cae9f1cbd4b96 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 12:23:26 +0100 Subject: [PATCH 41/77] New example on multi_queries --- doc/qbk/00_main.qbk | 2 +- doc/qbk/05_connection_establishment.qbk | 28 +++ doc/qbk/25_examples.qbk | 6 +- .../2_simple/multi_queries_transactions.cpp | 187 +++++++++++++++++ example/2_simple/transactions.cpp | 188 ------------------ example/CMakeLists.txt | 2 +- .../snippets/connection_establishment.cpp | 18 ++ tools/scripts/examples_qbk.py | 2 +- 8 files changed, 239 insertions(+), 194 deletions(-) create mode 100644 example/2_simple/multi_queries_transactions.cpp delete mode 100644 example/2_simple/transactions.cpp diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 53b873841..e1cb9d879 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -132,7 +132,7 @@ END [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/transactions.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] diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index 579a8d3ec..574a752c9 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -221,6 +221,34 @@ in [link mysql.multi_function.read_some_rows this section]. + +[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] + +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] + + + [heading Connection encoding and collation] When establishing a connection, you specify a numeric collation ID diff --git a/doc/qbk/25_examples.qbk b/doc/qbk/25_examples.qbk index d63644142..12875fa0e 100644 --- a/doc/qbk/25_examples.qbk +++ b/doc/qbk/25_examples.qbk @@ -28,7 +28,7 @@ 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.transactions Using transactions] +* [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] @@ -140,11 +140,11 @@ This example assumes you have gone through the [link mysql.examples.setup setup] -[section:transactions Using transactions] +[section:multi_queries_transactions Using multi-queries and transactions] This example assumes you have gone through the [link mysql.examples.setup setup]. -[example_transactions] +[example_multi_queries_transactions] [endsect] 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/2_simple/transactions.cpp b/example/2_simple/transactions.cpp deleted file mode 100644 index 64433cc23..000000000 --- a/example/2_simple/transactions.cpp +++ /dev/null @@ -1,188 +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 -#ifdef BOOST_ASIO_HAS_CO_AWAIT - -//[example_transactions - -/** - * This program shows how to start, commit and roll back transactions - * spanning multiple queries. - * - * The program modifies an order of an online store system, - * adding a new line item to it. The program must first check - * that the order is an editable state. - * - * 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. - */ - -#include -#include -#include -#include -#include -#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 server_hostname, - std::string username, - std::string password, - std::uint64_t order_id, - std::uint64_t product_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{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples", - .multi_queries = true, // Enable support for semicolon-separated queries - }; - - // Connect to the server - co_await conn.async_connect(params); - - // Open a transaction block with START TRANSACTION. - // Transaction statements are regular SQL, and can be issued using async_execute. - // Then retrieve the order and lock it so it doesn't get modified while we're examining it. - // We need to check whether the order is in a 'draft' state before adding items to it. - // We combine the transaction and the select statements to save round-trips to the server. - mysql::results result; - co_await conn.async_execute( - mysql::with_params( - "START TRANSACTION; " - "SELECT status FROM orders WHERE id = {} FOR SHARE", - order_id - ), - result - ); - - // We issued 2 queries, so we get 2 resultsets back. - // The 1st resultset corresponds to the START TRANSACTION and is empty. - // The 2nd resultset corresponds to the SELECT and contains our order. - // If a connection closes while a transaction is in progress, - // the transaction is rolled back. No ROLLBACK statement required. - mysql::rows_view orders = result.at(1).rows(); - if (orders.empty()) - { - std::cout << "Can't find order with id=" << order_id << std::endl; - exit(1); - } - - // Retrieve and check the order status - auto order_status = orders.at(0).at(0).as_string(); - if (order_status != "draft") - { - std::cout << "Order can't be modified because it's in " << order_status << " status\n"; - exit(1); - } - - // We're good to proceed. Insert the new order item and commit the transaction. - // If the INSERT fails, the COMMIT statement is not executed - // and the transaction is rolled back when the connection closes. - co_await conn.async_execute( - mysql::with_params( - "INSERT INTO order_items (order_id, product_id, quantity) VALUES ({}, {}, 1); " - "COMMIT", - order_id, - product_id - ), - 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 != 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]), std::stoi(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 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/CMakeLists.txt b/example/CMakeLists.txt index 097b9f216..6305f2830 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -87,7 +87,7 @@ 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(transactions ARGS ${REGULAR_ARGS} 1 1) +add_simple_example(multi_queries_transactions ARGS ${REGULAR_ARGS} 1 "John") diff --git a/test/integration/test/snippets/connection_establishment.cpp b/test/integration/test/snippets/connection_establishment.cpp index e13ecad90..4a7f3a4e6 100644 --- a/test/integration/test/snippets/connection_establishment.cpp +++ b/test/integration/test/snippets/connection_establishment.cpp @@ -7,10 +7,15 @@ #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; @@ -35,6 +40,19 @@ BOOST_FIXTURE_TEST_CASE(section_connection_establishment, io_context_fixture) // Connect and use the connection normally //] } +#ifdef BOOST_ASIO_HAS_CO_AWAIT + { + run_coro(ctx, []() -> asio::awaitable { + mysql::any_connection conn(co_await asio::this_coro::executor); + co_await conn.async_connect(snippets_connect_params()); + + //[section_connection_establishment_multi_queries_execute + mysql::results result; + co_await conn.async_execute("START TRANSACTION; SELECT 1; COMMIT", result); + //] + }); + } +#endif } } // namespace diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index 1b1822276..f9e278905 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -109,7 +109,7 @@ class MultiExample(NamedTuple): SIMPLE_EXAMPLES = [ Example('prepared_statements', '2_simple/prepared_statements.cpp', 'Prepared statements'), Example('timeouts', '2_simple/timeouts.cpp', 'Setting timeouts to operations'), - Example('transactions', '2_simple/transactions.cpp', 'Using transactions'), + 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'), From 1c64f57bd6479ad26971fd409affe8288bebef9f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 12:40:10 +0100 Subject: [PATCH 42/77] Closing connections --- doc/qbk/05_connection_establishment.qbk | 46 ++++++++----------------- doc/qbk/18_charsets.qbk | 31 +++++++++++++++++ 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index 574a752c9..eb99cebe3 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -235,6 +235,11 @@ Semicolon-separated queries are useful in a number of cases, like when using tra [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 @@ -249,38 +254,19 @@ need this feature should leave it off as a best practice. -[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. -] - - [section Closing a connection] -[endsect] - +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] @@ -302,8 +288,6 @@ instead of rolling out your own strategy. [endsect] -destroying a connection does not leak any resources -but calling async_close ensures a clean closing. This includes notifying the server, -so it closes the connection immediately, and cleanly shutting down TLS. + [endsect] [/ connparams] diff --git a/doc/qbk/18_charsets.qbk b/doc/qbk/18_charsets.qbk index 7bf383646..de28df8a7 100644 --- a/doc/qbk/18_charsets.qbk +++ b/doc/qbk/18_charsets.qbk @@ -125,6 +125,37 @@ The table below summarizes the encoding used by each piece of functionality in t ] ] +[heading During connection establishment] + +TODO: review this + +When establishing a connection, you specify a numeric collation ID +parameter ([refmem connect_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:tracking (Experimental) Character set tracking] [reflink any_connection] attempts to track the connection's current character set. From 8cc748c99fc1bf20888bd38684c72f7600be027d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 12:56:00 +0100 Subject: [PATCH 43/77] Finished connection establishment --- doc/qbk/05_connection_establishment.qbk | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index eb99cebe3..0b05e972a 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -270,20 +270,23 @@ cleanly when possible. -[section Reconnection] +[section Reconnection and long-lived connections] -[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`. +[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. -For instance, the following can be used to implement retries for connection establishment: +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. -[any_connection_reconnect] +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. -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. +TODO: should we talk about is_fatal_error? Move to error_handling? [endsect] From 381d5dcc0e9eb62bacc4ae2d3c4813338835b677 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 13:12:50 +0100 Subject: [PATCH 44/77] Remove reconnection --- doc/qbk/00_main.qbk | 1 - doc/qbk/17_reconnecting.qbk | 48 ----- doc/qbk/TODO_templated_connection.qbk | 44 ++++ test/integration/CMakeLists.txt | 1 - test/integration/Jamfile | 1 - .../test/snippets/any_connection.cpp | 201 ------------------ 6 files changed, 44 insertions(+), 252 deletions(-) delete mode 100644 doc/qbk/17_reconnecting.qbk delete mode 100644 test/integration/test/snippets/any_connection.cpp diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index e1cb9d879..43aab4032 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -192,7 +192,6 @@ END [include 12_async.qbk] [include 15_error_handling.qbk] [include 16_connparams.qbk] -[include 17_reconnecting.qbk] [include 18_charsets.qbk] [include 19_time_types.qbk] [include 21_connection_pool.qbk] 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/TODO_templated_connection.qbk b/doc/qbk/TODO_templated_connection.qbk index a0803f025..4cfd3038d 100644 --- a/doc/qbk/TODO_templated_connection.qbk +++ b/doc/qbk/TODO_templated_connection.qbk @@ -184,4 +184,48 @@ roughly equivalent to [refmem connection close]: * 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/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index c3c6ad176..3d7e1234d 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -43,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 diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 47cf884f0..eb95fdca1 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -55,7 +55,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/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 From d2c442d2df07db050cb380461cdcababcb0ded0f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 13:18:18 +0100 Subject: [PATCH 45/77] Reorder --- doc/qbk/{07_queries.qbk => 06_text_queries.qbk} | 2 +- .../{08_prepared_statements.qbk => 07_prepared_statements.qbk} | 0 doc/qbk/{05_dynamic_interface.qbk => 08_dynamic_interface.qbk} | 0 doc/qbk/{06_static_interface.qbk => 09_static_interface.qbk} | 0 doc/qbk/{09_multi_resultset.qbk => 10_multi_resultset.qbk} | 0 doc/qbk/{10_multi_function.qbk => 11_multi_function.qbk} | 0 doc/qbk/{21_connection_pool.qbk => 12_connection_pool.qbk} | 0 doc/qbk/{12_async.qbk => 13_async.qbk} | 0 doc/qbk/{11_metadata.qbk => 14_metadata.qbk} | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename doc/qbk/{07_queries.qbk => 06_text_queries.qbk} (97%) rename doc/qbk/{08_prepared_statements.qbk => 07_prepared_statements.qbk} (100%) rename doc/qbk/{05_dynamic_interface.qbk => 08_dynamic_interface.qbk} (100%) rename doc/qbk/{06_static_interface.qbk => 09_static_interface.qbk} (100%) rename doc/qbk/{09_multi_resultset.qbk => 10_multi_resultset.qbk} (100%) rename doc/qbk/{10_multi_function.qbk => 11_multi_function.qbk} (100%) rename doc/qbk/{21_connection_pool.qbk => 12_connection_pool.qbk} (100%) rename doc/qbk/{12_async.qbk => 13_async.qbk} (100%) rename doc/qbk/{11_metadata.qbk => 14_metadata.qbk} (100%) diff --git a/doc/qbk/07_queries.qbk b/doc/qbk/06_text_queries.qbk similarity index 97% rename from doc/qbk/07_queries.qbk rename to doc/qbk/06_text_queries.qbk index 12883d520..aaca6de7f 100644 --- a/doc/qbk/07_queries.qbk +++ b/doc/qbk/06_text_queries.qbk @@ -5,7 +5,7 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) ] -[section:queries Text queries] +[section:text_queries Text queries and client-side SQL formatting] 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: 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/05_dynamic_interface.qbk b/doc/qbk/08_dynamic_interface.qbk similarity index 100% rename from doc/qbk/05_dynamic_interface.qbk rename to doc/qbk/08_dynamic_interface.qbk diff --git a/doc/qbk/06_static_interface.qbk b/doc/qbk/09_static_interface.qbk similarity index 100% rename from doc/qbk/06_static_interface.qbk rename to doc/qbk/09_static_interface.qbk 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 100% rename from doc/qbk/12_async.qbk rename to doc/qbk/13_async.qbk diff --git a/doc/qbk/11_metadata.qbk b/doc/qbk/14_metadata.qbk similarity index 100% rename from doc/qbk/11_metadata.qbk rename to doc/qbk/14_metadata.qbk From 1bdb6761deb6c9e4d82a0bf290c835b2bacd7821 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 14:17:43 +0100 Subject: [PATCH 46/77] Rework text queries --- doc/qbk/00_main.qbk | 13 +- ...l_formatting.qbk => 06_sql_formatting.qbk} | 347 ++---------------- doc/qbk/06_text_queries.qbk | 53 --- doc/qbk/23_sql_formatting_advanced.qbk | 302 ++++++++++++++- 4 files changed, 333 insertions(+), 382 deletions(-) rename doc/qbk/{22_sql_formatting.qbk => 06_sql_formatting.qbk} (51%) delete mode 100644 doc/qbk/06_text_queries.qbk diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 43aab4032..180cb9a49 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -160,7 +160,6 @@ END [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/any_connection.cpp] [import ../../test/integration/test/snippets/tutorials.cpp] [import ../../test/integration/test/snippets/metadata.cpp] [import ../../test/integration/test/snippets/connection_pool.cpp] @@ -180,11 +179,14 @@ END [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_connection_establishment.qbk] -[/ [include 05_dynamic_interface.qbk] +[include 06_sql_formatting.qbk] +[include 23_sql_formatting_advanced.qbk] +[include 25_examples.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] @@ -195,10 +197,7 @@ END [include 18_charsets.qbk] [include 19_time_types.qbk] [include 21_connection_pool.qbk] -[include 22_sql_formatting.qbk] -[include 23_sql_formatting_advanced.qbk] [include 24_pipeline.qbk] ] -[include 25_examples.qbk] [/ [section:ref Reference] diff --git a/doc/qbk/22_sql_formatting.qbk b/doc/qbk/06_sql_formatting.qbk similarity index 51% rename from doc/qbk/22_sql_formatting.qbk rename to doc/qbk/06_sql_formatting.qbk index 5111236cc..930a66fe2 100644 --- a/doc/qbk/22_sql_formatting.qbk +++ b/doc/qbk/06_sql_formatting.qbk @@ -5,21 +5,33 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) ] -[section:sql_formatting Client-side SQL query formatting] +[section:sql_formatting Text queries and client-side SQL 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. - +['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] -[heading 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.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 @@ -61,12 +73,14 @@ Like `std::format`, you can use arguments with explicit indices: See [link mysql.sql_formatting_advanced.format_string_syntax this section] for a reference on the format string syntax. +[endsect] + -[heading:errors Common errors and how to fix them] +[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 @@ -96,96 +110,12 @@ For example: [sql_formatting_invalid_encoding] +[endsect] -[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] +[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 @@ -250,8 +180,6 @@ Both client-side SQL formatting and prepared statements have pros and cons effic - - [heading:security Security considerations] Both client-side SQL formatting and prepared statements [*protect against SQL injection]. @@ -287,227 +215,6 @@ To sum up: Use [refmem any_connection format_opts], instead. +[endsect] - - - - - - - - - -[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 +[endsect] diff --git a/doc/qbk/06_text_queries.qbk b/doc/qbk/06_text_queries.qbk deleted file mode 100644 index aaca6de7f..000000000 --- a/doc/qbk/06_text_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:text_queries Text queries and client-side SQL formatting] - -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/23_sql_formatting_advanced.qbk b/doc/qbk/23_sql_formatting_advanced.qbk index 1b1501174..5d71a406f 100644 --- a/doc/qbk/23_sql_formatting_advanced.qbk +++ b/doc/qbk/23_sql_formatting_advanced.qbk @@ -9,7 +9,87 @@ [nochunk] -[heading Extending format_sql] +[section 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. + +[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: @@ -31,7 +111,7 @@ We can now use it like this: See the [reflink formatter] reference docs for more info. - +[endsect] @@ -86,6 +166,7 @@ Otherwise, an error will be generated. + [heading:error_handling Error handling model] Some values can't be securely formatted. For instance, C++ @@ -123,6 +204,7 @@ 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 @@ -160,6 +242,7 @@ For a reference on how character set tracking works, please read [link mysql.cha + [heading Custom string types] [reflink format_sql_to] can be used with string types that are not `std::string`, @@ -196,5 +279,220 @@ which mimics [@https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-escape-string.h +[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] From 29c9ffe12c3767c47d5bd7bd1d37c1e96a615948 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 15:53:53 +0100 Subject: [PATCH 47/77] Final qbk reorder --- doc/qbk/00_main.qbk | 32 +++++++++---------- doc/qbk/10_multi_resultset.qbk | 2 ++ ...ror_handling.qbk => 14_error_handling.qbk} | 0 ...ced.qbk => 15_sql_formatting_advanced.qbk} | 0 doc/qbk/{14_metadata.qbk => 16_metadata.qbk} | 0 doc/qbk/{18_charsets.qbk => 17_charsets.qbk} | 0 .../{19_time_types.qbk => 18_time_types.qbk} | 0 ...ection.qbk => 19_templated_connection.qbk} | 0 doc/qbk/{24_pipeline.qbk => 20_pipeline.qbk} | 0 doc/qbk/{25_examples.qbk => 21_examples.qbk} | 0 10 files changed, 18 insertions(+), 16 deletions(-) rename doc/qbk/{15_error_handling.qbk => 14_error_handling.qbk} (100%) rename doc/qbk/{23_sql_formatting_advanced.qbk => 15_sql_formatting_advanced.qbk} (100%) rename doc/qbk/{14_metadata.qbk => 16_metadata.qbk} (100%) rename doc/qbk/{18_charsets.qbk => 17_charsets.qbk} (100%) rename doc/qbk/{19_time_types.qbk => 18_time_types.qbk} (100%) rename doc/qbk/{TODO_templated_connection.qbk => 19_templated_connection.qbk} (100%) rename doc/qbk/{24_pipeline.qbk => 20_pipeline.qbk} (100%) rename doc/qbk/{25_examples.qbk => 21_examples.qbk} (100%) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 180cb9a49..c8616b295 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -179,25 +179,25 @@ END [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_connection_establishment.qbk] [include 06_sql_formatting.qbk] -[include 23_sql_formatting_advanced.qbk] -[include 25_examples.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] +[include 19_templated_connection.qbk] +[include 20_pipeline.qbk] +[include 21_examples.qbk] -[/ [include 04_overview.qbk] -[include 05_dynamic_interface.qbk] -[include 06_static_interface.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 15_error_handling.qbk] -[include 16_connparams.qbk] -[include 18_charsets.qbk] -[include 19_time_types.qbk] -[include 21_connection_pool.qbk] -[include 24_pipeline.qbk] ] [/ [section:ref Reference] diff --git a/doc/qbk/10_multi_resultset.qbk b/doc/qbk/10_multi_resultset.qbk index 23eecdcfb..8c92c0850 100644 --- a/doc/qbk/10_multi_resultset.qbk +++ b/doc/qbk/10_multi_resultset.qbk @@ -136,4 +136,6 @@ Note that statements like `DELIMITER` [*do not work] using this feature. This is You can also use the static interface with multi-queries. It works the same as with stored procedures. +TODO: expand this. + [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/23_sql_formatting_advanced.qbk b/doc/qbk/15_sql_formatting_advanced.qbk similarity index 100% rename from doc/qbk/23_sql_formatting_advanced.qbk rename to doc/qbk/15_sql_formatting_advanced.qbk diff --git a/doc/qbk/14_metadata.qbk b/doc/qbk/16_metadata.qbk similarity index 100% rename from doc/qbk/14_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 100% rename from doc/qbk/18_charsets.qbk rename to doc/qbk/17_charsets.qbk 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/TODO_templated_connection.qbk b/doc/qbk/19_templated_connection.qbk similarity index 100% rename from doc/qbk/TODO_templated_connection.qbk rename to doc/qbk/19_templated_connection.qbk 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/25_examples.qbk b/doc/qbk/21_examples.qbk similarity index 100% rename from doc/qbk/25_examples.qbk rename to doc/qbk/21_examples.qbk From 0657634886d1c8f3b025f15dd80ce442a3bf92f0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:06:38 +0100 Subject: [PATCH 48/77] Overview rework --- doc/qbk/04_overview.qbk | 320 ++++++++++-------- doc/qbk/05_connection_establishment.qbk | 2 +- doc/qbk/15_sql_formatting_advanced.qbk | 2 +- test/integration/test/snippets/overview.cpp | 355 +++++++------------- tools/scripts/examples_qbk.py | 2 +- 5 files changed, 312 insertions(+), 369 deletions(-) diff --git a/doc/qbk/04_overview.qbk b/doc/qbk/04_overview.qbk index 646d1d069..aae91f90f 100644 --- a/doc/qbk/04_overview.qbk +++ b/doc/qbk/04_overview.qbk @@ -9,107 +9,160 @@ [nochunk] +[/ Merge connection and connection establishment +Running simple queries and retrieving rows +Running queries with parameters +Parsing into custom types with the static interface (include comparison) +Prepared statements +Sync, async and the universal model +Connection pools: rewrite to be more goal-oriented: how to use UNIX, disable TLS, set up custom TLS options +Multi-function operations ] + + This section briefly explains the library main classes and functions, and how to use them. -[section:connection Connection objects] +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. -[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 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 [link mysql.templated_connection 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.metadata 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 (TODO: duplicated): -[overview_ifaces_table] +[!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 +177,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 +190,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] ] ] ] -[endsect] +Prefer using the static interface when possible. -[section Resultsets] +[endsect] -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 diagnostic] 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 +292,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 +311,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 index 0b05e972a..4f1ca66b6 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -174,7 +174,7 @@ during pool construction, as members of [reflink pool_params]: -[section UNIX sockets] +[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. diff --git a/doc/qbk/15_sql_formatting_advanced.qbk b/doc/qbk/15_sql_formatting_advanced.qbk index 5d71a406f..be90d16a3 100644 --- a/doc/qbk/15_sql_formatting_advanced.qbk +++ b/doc/qbk/15_sql_formatting_advanced.qbk @@ -9,7 +9,7 @@ [nochunk] -[section Formatting queries without executing them] +[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 diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index 1f20e1572..31b025f88 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -5,144 +5,139 @@ // 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/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" +#include "test_integration/snippets/snippets_fixture.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) +//[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; + std::int64_t id; + std::string first_name; + std::string last_name; +}; +//] - // 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); - - // Represents a connection to the MySQL server. - boost::mysql::tcp_ssl_connection conn(ctx.get_executor(), ssl_ctx); - //] - - //[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 - ); + auto& ctx = static_cast((co_await asio::this_coro::executor).context()); - // Connect to the server using the first endpoint returned by the resolver - conn.connect(*endpoints.begin(), params); + //[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_query_use_case - results result; - conn.execute("START TRANSACTION", 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_statement_use_case - statement stmt = conn.prepare_statement( - "SELECT first_name FROM employee WHERE company_id = ? AND salary > ?" + //[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 ); - - results result; - conn.execute(stmt.bind("HGS", 30000), 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_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 +145,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, title, body FROM posts", 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 +186,62 @@ 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 + } } -BOOST_ATTRIBUTE_UNUSED -void section_async_cpp11(boost::asio::yield_context yield) +BOOST_FIXTURE_TEST_CASE(section_overview, snippets_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/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index f9e278905..a03b20d99 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -212,7 +212,7 @@ def main(): # Write to output file _replace_imports(import_contents) - _write_file(['doc', 'qbk', '25_examples.qbk'], example_contents) + _write_file(['doc', 'qbk', '21_examples.qbk'], example_contents) if __name__ == '__main__': From a32272b1099e36abbea606c3ceb6d5eed72915b4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:14:45 +0100 Subject: [PATCH 49/77] Comment templated connection --- doc/qbk/00_main.qbk | 3 ++- doc/qbk/19_templated_connection.qbk | 18 ++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index c8616b295..3c8abdf0f 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -194,7 +194,8 @@ END [include 16_metadata.qbk] [include 17_charsets.qbk] [include 18_time_types.qbk] -[include 19_templated_connection.qbk] +[/ TODO: re-enable this +[include 19_templated_connection.qbk] ] [include 20_pipeline.qbk] [include 21_examples.qbk] diff --git a/doc/qbk/19_templated_connection.qbk b/doc/qbk/19_templated_connection.qbk index 4cfd3038d..39d470e8a 100644 --- a/doc/qbk/19_templated_connection.qbk +++ b/doc/qbk/19_templated_connection.qbk @@ -14,8 +14,7 @@ You may encounter code using [reflink connection] or its aliases, main way to create client connections until Boost 1.87, when [reflink any_connection] became stable. -Current status: `connection` and aliases are still maintained -(not deprecated), but we don't recommend using them in new code. +`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. @@ -23,10 +22,11 @@ level of efficiency. [heading Streams and type aliases] -[reflink connection] is templated on the [reflink Stream] class. -That is, the transport can be configured. +[reflink connection] is templated on the [reflink Stream] class, +which implements the transport layer to read and write bytes +from the wire. -This library provides helper type aliases for the most common cases: +The library provides helper type aliases for the most common cases: [table [ @@ -70,18 +70,16 @@ The same three transports above can be used with `any_connection`. For a [reflink tcp_ssl_connection], we need to pass an execution context and a __ssl_context__: -[overview_connection] TODO: adapt +[templated_connection_creation] -In contrast, when using [reflink any_connection], passing a -__ssl_context__ is not mandatory. -One will be created for you if you didn't pass one and the connection -uses TLS. [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) From 1be72862901a73f81223a384c6a1ab7b21e3e667 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:23:19 +0100 Subject: [PATCH 50/77] Remove SET NAMES recommendatins --- doc/qbk/00_main.qbk | 1 - doc/qbk/17_charsets.qbk | 50 +++++---------------- test/integration/test/snippets/charsets.cpp | 11 ----- 3 files changed, 10 insertions(+), 52 deletions(-) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 3c8abdf0f..24ab430ce 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -66,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`]] diff --git a/doc/qbk/17_charsets.qbk b/doc/qbk/17_charsets.qbk index de28df8a7..eca8ba49d 100644 --- a/doc/qbk/17_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,38 +126,7 @@ The table below summarizes the encoding used by each piece of functionality in t ] ] -[heading During connection establishment] - -TODO: review this - -When establishing a connection, you specify a numeric collation ID -parameter ([refmem connect_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: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 @@ -197,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/test/integration/test/snippets/charsets.cpp b/test/integration/test/snippets/charsets.cpp index 95e270db3..ca5983f4c 100644 --- a/test/integration/test/snippets/charsets.cpp +++ b/test/integration/test/snippets/charsets.cpp @@ -11,8 +11,6 @@ #include #include -#include "test_integration/snippets/get_connection.hpp" - using namespace boost::mysql; using namespace boost::mysql::test; @@ -68,15 +66,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}; From fdba7b0ed7105eab8149224a58cf7585b5cea723 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:24:23 +0100 Subject: [PATCH 51/77] Further SET NAMES references --- doc/qbk/08_dynamic_interface.qbk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/qbk/08_dynamic_interface.qbk b/doc/qbk/08_dynamic_interface.qbk index eca9215cb..8cabc8971 100644 --- a/doc/qbk/08_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] From a0dbb5efc7a52c5b6fb1414ae61a55448ab404d1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:31:28 +0100 Subject: [PATCH 52/77] TODO cleanup --- doc/qbk/03_1_tutorial_sync.qbk | 4 ++-- doc/qbk/03_2_tutorial_async.qbk | 7 ++++--- doc/qbk/03_3_tutorial_with_params.qbk | 3 +-- doc/qbk/03_4_tutorial_static_interface.qbk | 8 ++++---- doc/qbk/04_overview.qbk | 14 +++----------- doc/qbk/05_connection_establishment.qbk | 2 -- doc/qbk/10_multi_resultset.qbk | 2 -- 7 files changed, 14 insertions(+), 26 deletions(-) diff --git a/doc/qbk/03_1_tutorial_sync.qbk b/doc/qbk/03_1_tutorial_sync.qbk index 9fdbdbb74..c8f2944c4 100644 --- a/doc/qbk/03_1_tutorial_sync.qbk +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -112,8 +112,8 @@ we are closing the connection, and thus may fail. [heading Next steps] -TODO: link to the next one. - 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 index accae63a3..9fe86ced9 100644 --- a/doc/qbk/03_2_tutorial_async.qbk +++ b/doc/qbk/03_2_tutorial_async.qbk @@ -71,7 +71,8 @@ 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. -There are ways to prevent this (TODO: link to it) +You can prevent this by [link mysql.overview.errors using `asio::redirect_error`]. + @@ -97,8 +98,8 @@ thread until all the scheduled coroutines and I/O operations complete: [heading Next steps] -TODO: link to the next one. - 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 index fdc9fb2e5..866de1490 100644 --- a/doc/qbk/03_3_tutorial_with_params.qbk +++ b/doc/qbk/03_3_tutorial_with_params.qbk @@ -143,8 +143,7 @@ With all these changes, this is how our coroutine looks like: Full program listing for this tutorial is [link mysql.examples.tutorial_with_params here]. - -TODO: link to the next one. +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 index 6235871e1..428e86888 100644 --- a/doc/qbk/03_4_tutorial_static_interface.qbk +++ b/doc/qbk/03_4_tutorial_static_interface.qbk @@ -92,8 +92,7 @@ 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. -See (TODO: link to discussion or example) -for sample code. + [heading Wrapping up] @@ -102,7 +101,8 @@ for sample code. Full program listing for this tutorial is [link mysql.examples.tutorial_static_interface here]. - -TODO: explain that this is over and point to other resources. +This concludes our tutorial series. You can now look at the [link mysql.overview overview section] +to learn more about the library features, or to the [link mysql.examples example section] +if you prefer to learn by doing. [endsect] \ No newline at end of file diff --git a/doc/qbk/04_overview.qbk b/doc/qbk/04_overview.qbk index aae91f90f..93c7331a4 100644 --- a/doc/qbk/04_overview.qbk +++ b/doc/qbk/04_overview.qbk @@ -9,16 +9,6 @@ [nochunk] -[/ Merge connection and connection establishment -Running simple queries and retrieving rows -Running queries with parameters -Parsing into custom types with the static interface (include comparison) -Prepared statements -Sync, async and the universal model -Connection pools: rewrite to be more goal-oriented: how to use UNIX, disable TLS, set up custom TLS options -Multi-function operations ] - - This section briefly explains the library main classes and functions, and how to use them. Boost.MySQL exposes sync and async functions implementing functionality involving I/O. @@ -152,7 +142,9 @@ and the library will parse the retrieved values for you into the types you provi You can use almost every feature in this library (including text queries and prepared statements) with both interfaces. -For example, given the following table (TODO: duplicated): +For example, given the following table : + +[/ (TODO: this code is duplicated.) ] [!teletype] ``` diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index 4f1ca66b6..167ed35c5 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -286,8 +286,6 @@ If you need reliable, long-lived connections, consider instead of rolling out your own strategy. `connection_pool` takes care of re-connecting and re-using connections for you. -TODO: should we talk about is_fatal_error? Move to error_handling? - [endsect] diff --git a/doc/qbk/10_multi_resultset.qbk b/doc/qbk/10_multi_resultset.qbk index 8c92c0850..23eecdcfb 100644 --- a/doc/qbk/10_multi_resultset.qbk +++ b/doc/qbk/10_multi_resultset.qbk @@ -136,6 +136,4 @@ Note that statements like `DELIMITER` [*do not work] using this feature. This is You can also use the static interface with multi-queries. It works the same as with stored procedures. -TODO: expand this. - [endsect] \ No newline at end of file From fe70e9d5b80e068b57f48b9f5bd3821c4a3f9aa5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:33:05 +0100 Subject: [PATCH 53/77] PFR guard --- example/1_tutorial/4_static_interface.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/1_tutorial/4_static_interface.cpp b/example/1_tutorial/4_static_interface.cpp index 1f18d6911..6308ee8cc 100644 --- a/example/1_tutorial/4_static_interface.cpp +++ b/example/1_tutorial/4_static_interface.cpp @@ -5,8 +5,10 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include + #include -#ifdef BOOST_ASIO_HAS_CO_AWAIT +#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED //[example_tutorial_static_interface From a52606a9a69a6363a687c34ef362e80f509e07be Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:51:49 +0100 Subject: [PATCH 54/77] Move source_script --- example/{3_advanced => 2_simple}/source_script.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example/{3_advanced => 2_simple}/source_script.cpp (100%) diff --git a/example/3_advanced/source_script.cpp b/example/2_simple/source_script.cpp similarity index 100% rename from example/3_advanced/source_script.cpp rename to example/2_simple/source_script.cpp From 523c3bf8af5527ffd727727d385ae663f20f65bd Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:52:05 +0100 Subject: [PATCH 55/77] Remove old orders examples --- example/order_management/db_setup.sql | 314 ---------- example/order_management/parse_cmdline.hpp | 200 ------- .../prepared_statements_cpp11.cpp | 437 -------------- .../prepared_statements_cpp14.cpp | 538 ------------------ .../stored_procedures_cpp11.cpp | 306 ---------- .../stored_procedures_cpp14.cpp | 401 ------------- 6 files changed, 2196 deletions(-) delete mode 100644 example/order_management/db_setup.sql delete mode 100644 example/order_management/parse_cmdline.hpp delete mode 100644 example/order_management/prepared_statements_cpp11.cpp delete mode 100644 example/order_management/prepared_statements_cpp14.cpp delete mode 100644 example/order_management/stored_procedures_cpp11.cpp delete mode 100644 example/order_management/stored_procedures_cpp14.cpp 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 - -//] From 8b4541f4adfbe9f74d254feb940e0c30c103cbbd Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 19:52:18 +0100 Subject: [PATCH 56/77] Final example cmake --- example/CMakeLists.txt | 199 +++++++---------------- example/private/run_stored_procedures.py | 65 -------- 2 files changed, 59 insertions(+), 205 deletions(-) delete mode 100644 example/private/run_stored_procedures.py diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 6305f2830..57646289d 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -22,64 +22,61 @@ 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) - 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} - ) -endfunction() -function(add_example EXAMPLE_NAME EXAMPLE_PATH) - set(MULTI_VALUE_ARGS LIBS ARGS) - cmake_parse_arguments(ADD_EXAMPLE "" "" "${MULTI_VALUE_ARGS}" ${ARGN}) +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_example_target(${TARGET_NAME} ${EXAMPLE_PATH}) + 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_test( - NAME ${TARGET_NAME} - COMMAND ${TARGET_NAME} ${ADD_EXAMPLE_ARGS} - ) + + # 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() -function(add_simple_example EXAMPLE_NAME) - add_example(${EXAMPLE_NAME} "2_simple/${EXAMPLE_NAME}.cpp" ${ARGN}) +function(add_tutorial EXAMPLE_NAME EXAMPLE_PATH) + add_example(${EXAMPLE_NAME} SOURCES "1_tutorial/${EXAMPLE_PATH}" ${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() +function(add_simple_example EXAMPLE_NAME) + add_example(${EXAMPLE_NAME} SOURCES "2_simple/${EXAMPLE_NAME}.cpp" ${ARGN}) +endfunction() set(REGULAR_ARGS example_user example_password ${SERVER_HOST}) # Tutorials -add_example(tutorial_sync 1_tutorial/1_sync.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_async 1_tutorial/2_async.cpp ARGS ${REGULAR_ARGS}) -add_example(tutorial_with_params 1_tutorial/3_with_params.cpp ARGS ${REGULAR_ARGS} 1) -add_example(tutorial_static_interface 1_tutorial/4_static_interface.cpp ARGS ${REGULAR_ARGS} 1 LIBS Boost::pfr) +add_tutorial(tutorial_sync 1_sync.cpp ARGS ${REGULAR_ARGS}) +add_tutorial(tutorial_async 2_async.cpp ARGS ${REGULAR_ARGS}) +add_tutorial(tutorial_with_params 3_with_params.cpp ARGS ${REGULAR_ARGS} 1) +add_tutorial(tutorial_static_interface 4_static_interface.cpp ARGS ${REGULAR_ARGS} 1 LIBS Boost::pfr) # 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 ${REGULAR_ARGS} LIBS Boost::json) -add_simple_example(batch_inserts_generic ARGS ${REGULAR_ARGS} LIBS Boost::json) -add_simple_example(patch_updates ARGS ${REGULAR_ARGS}) -add_simple_example(dynamic_filters ARGS ${REGULAR_ARGS}) -add_simple_example(unix_socket ARGS example_user example_password) +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}) @@ -88,103 +85,25 @@ 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_simple_example(unix_socket ARGS example_user example_password) +endif() - -# # Regular examples are the ones that require no extra linking libs and can be run -# # with example_user example_password -# set(REGULAR_EXAMPLES -# tutorial -# tutorial_async -# simple/callbacks -# simple/ -# 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) - -# # 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 -# ) -# endif() - -# # Source script -# 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 -# ) -# boost_mysql_common_target_settings(boost_mysql_example_connection_pool) -# run_python_example(boost_mysql_example_connection_pool run_connection_pool.py) +# Advanced +add_example( + 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 +) 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() From 10332ff5270ab9f4e427f36b057bba22f94041a0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:09:14 +0100 Subject: [PATCH 57/77] charsets fix --- test/integration/test/snippets/charsets.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/integration/test/snippets/charsets.cpp b/test/integration/test/snippets/charsets.cpp index ca5983f4c..a00f85c36 100644 --- a/test/integration/test/snippets/charsets.cpp +++ b/test/integration/test/snippets/charsets.cpp @@ -5,15 +5,14 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include + #include #include #include #include -using namespace boost::mysql; -using namespace boost::mysql::test; - namespace { //[charsets_next_char From 615b7eccecf24b50944b6459efd7ef82c5578b8d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:40:06 +0100 Subject: [PATCH 58/77] Fix patch_updates --- example/2_simple/patch_updates.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/2_simple/patch_updates.cpp b/example/2_simple/patch_updates.cpp index cf5bc2be5..864fecf02 100644 --- a/example/2_simple/patch_updates.cpp +++ b/example/2_simple/patch_updates.cpp @@ -159,12 +159,12 @@ asio::awaitable coro_main(const cmdline_args& args) mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::string(args.server_hostname)), - .username = std::string(args.username), - .password = std::string(args.password), - .database = "boost_mysql_examples" - }; + mysql::connect_params params; + params.server_address.emplace_host_and_port(std::string(args.server_hostname)); + params.username = args.username; + params.password = std::string(args.password); + params.database = "boost_mysql_examples"; + params.multi_queries = true; // Connect to the server co_await conn.async_connect(params); From 2847ce63fcc9337cba41809844b300d1b58d05b1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:40:46 +0100 Subject: [PATCH 59/77] Fix callbacks --- example/2_simple/callbacks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/2_simple/callbacks.cpp b/example/2_simple/callbacks.cpp index 4c975e25e..c165f833c 100644 --- a/example/2_simple/callbacks.cpp +++ b/example/2_simple/callbacks.cpp @@ -100,7 +100,7 @@ class session : public std::enable_shared_from_this // 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 = ?", + "SELECT first_name, last_name, salary FROM employee WHERE company_id = {}", company_id ), result, From 8db4d0803f810f403634e52de4a9c457bd2b6bcc Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:41:29 +0100 Subject: [PATCH 60/77] Fix callbacks --- example/2_simple/coroutines_cpp11.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/2_simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp index bef4d5afb..3830caa80 100644 --- a/example/2_simple/coroutines_cpp11.cpp +++ b/example/2_simple/coroutines_cpp11.cpp @@ -76,7 +76,7 @@ void coro_main( mysql::results result; conn.async_execute( mysql::with_params( - "SELECT first_name, last_name, salary FROM employee WHERE company_id = ?", + "SELECT first_name, last_name, salary FROM employee WHERE company_id = {}", company_id ), result, From 423ce911187bf8317a8e189960e3c88f05d31fd4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:42:53 +0100 Subject: [PATCH 61/77] workaround certificate verification --- example/2_simple/tls_certificate_verification.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example/2_simple/tls_certificate_verification.cpp b/example/2_simple/tls_certificate_verification.cpp index 13b9488f4..52225c4bd 100644 --- a/example/2_simple/tls_certificate_verification.cpp +++ b/example/2_simple/tls_certificate_verification.cpp @@ -89,9 +89,10 @@ asio::awaitable coro_main( // 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 equal to the configured hostname. + // 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(asio::ssl::host_name_verification(std::string(server_hostname))); + // 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. From c4357ace9ec7325659c812d96bca06927f0baf58 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:43:32 +0100 Subject: [PATCH 62/77] Fix connection_pool --- example/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 57646289d..e91fe083b 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -106,4 +106,5 @@ add_example( Boost::url Boost::beast PYTHON_RUNNER run_connection_pool.py + ARGS ${SERVER_HOST} ) From 932e4886cb628a893e98e3382a1fd54145755ed9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 20:49:46 +0100 Subject: [PATCH 63/77] Fix snippets --- test/integration/test/snippets/overview.cpp | 2 +- test/integration/test/snippets/tutorials.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index 31b025f88..fad3a9d60 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -147,7 +147,7 @@ asio::awaitable overview_coro(mysql::any_connection& conn) // Passing a static_results to async_execute selects the static interface mysql::static_results> result; - co_await conn.async_execute("SELECT id, title, body FROM posts", 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 employee& emp : result.rows()) diff --git a/test/integration/test/snippets/tutorials.cpp b/test/integration/test/snippets/tutorials.cpp index b5a678e6d..7c652b01e 100644 --- a/test/integration/test/snippets/tutorials.cpp +++ b/test/integration/test/snippets/tutorials.cpp @@ -27,7 +27,7 @@ BOOST_FIXTURE_TEST_CASE(section_tutorials, snippets_fixture) { { mysql::results result; - conn.execute("SELECT first_name, last_name FROM employee WHERE id = 0", 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); From 1433955d4f34d16fa6113eaecdb8a84ca6f9dbb9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:00:15 +0100 Subject: [PATCH 64/77] Fix missing include --- example/2_simple/batch_inserts_generic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/example/2_simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp index 98f24feba..5b6ed80c8 100644 --- a/example/2_simple/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include From 4d0a9b6240c222112dd706f9648c761e2792acab Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:00:24 +0100 Subject: [PATCH 65/77] Example jamfile --- example/Jamfile | 82 +++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 47 deletions(-) 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 From 50480bbbbea237b40e0870ceb4c9de5a0e7dc7e8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:25:26 +0100 Subject: [PATCH 66/77] Removed order_management db_setup --- tools/ci/ci_util/db_setup.py | 1 - 1 file changed, 1 deletion(-) 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')) From d409ac9239a92b451b060ac4b2455812f4b4180c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:40:28 +0100 Subject: [PATCH 67/77] Remove named initializers --- example/2_simple/batch_inserts.cpp | 17 ++++++++--------- example/2_simple/batch_inserts_generic.cpp | 17 ++++++++--------- example/2_simple/dynamic_filters.cpp | 11 +++++------ example/2_simple/metadata.cpp | 19 ++++++++++--------- example/2_simple/multi_function.cpp | 17 ++++++++++------- example/2_simple/pipeline.cpp | 17 ++++++++--------- example/2_simple/prepared_statements.cpp | 17 ++++++++--------- example/2_simple/timeouts.cpp | 17 ++++++++--------- 8 files changed, 65 insertions(+), 67 deletions(-) diff --git a/example/2_simple/batch_inserts.cpp b/example/2_simple/batch_inserts.cpp index 7657e0618..5327dd7d9 100644 --- a/example/2_simple/batch_inserts.cpp +++ b/example/2_simple/batch_inserts.cpp @@ -84,9 +84,9 @@ static std::string read_file(const char* file_name) // The main coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::span employees ) { @@ -95,12 +95,11 @@ asio::awaitable coro_main( mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp index 5b6ed80c8..03f01f716 100644 --- a/example/2_simple/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -132,9 +132,9 @@ std::string read_file(const char* file_name) // The main coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::span employees ) { @@ -143,12 +143,11 @@ asio::awaitable coro_main( mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/dynamic_filters.cpp b/example/2_simple/dynamic_filters.cpp index e8a0342d5..8399ba81b 100644 --- a/example/2_simple/dynamic_filters.cpp +++ b/example/2_simple/dynamic_filters.cpp @@ -256,12 +256,11 @@ asio::awaitable coro_main(const cmdline_args& args) mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::string(args.server_hostname)), - .username = std::string(args.username), - .password = std::string(args.password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/metadata.cpp b/example/2_simple/metadata.cpp index 59b0981b6..f69154164 100644 --- a/example/2_simple/metadata.cpp +++ b/example/2_simple/metadata.cpp @@ -37,7 +37,11 @@ namespace asio = boost::asio; namespace mysql = boost::mysql; // The main coroutine -asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) +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. @@ -49,14 +53,11 @@ asio::awaitable coro_main(std::string server_hostname, std::string usernam conn.set_meta_mode(mysql::metadata_mode::full); // The socket path, username, password and database to use. - // Passing ssl_mode::disable will disable the use of TLS. - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples", - .ssl = mysql::ssl_mode::disable, - }; + 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); diff --git a/example/2_simple/multi_function.cpp b/example/2_simple/multi_function.cpp index 9355e309b..fd8845029 100644 --- a/example/2_simple/multi_function.cpp +++ b/example/2_simple/multi_function.cpp @@ -43,18 +43,21 @@ void print_employee(mysql::row_view employee) } // The main coroutine -asio::awaitable coro_main(std::string server_hostname, std::string username, std::string password) +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{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/pipeline.cpp b/example/2_simple/pipeline.cpp index aaadf05fb..225fc764d 100644 --- a/example/2_simple/pipeline.cpp +++ b/example/2_simple/pipeline.cpp @@ -74,9 +74,9 @@ asio::awaitable> batch_prepare( // The main coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::string_view company_id ) { @@ -85,12 +85,11 @@ asio::awaitable coro_main( mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/prepared_statements.cpp b/example/2_simple/prepared_statements.cpp index 71564dd2c..8c1cb4c0f 100644 --- a/example/2_simple/prepared_statements.cpp +++ b/example/2_simple/prepared_statements.cpp @@ -46,9 +46,9 @@ void print_employee(mysql::row_view employee) // The main coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::string_view company_id ) { @@ -56,12 +56,11 @@ asio::awaitable coro_main( mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); diff --git a/example/2_simple/timeouts.cpp b/example/2_simple/timeouts.cpp index 42b15b105..6078e1088 100644 --- a/example/2_simple/timeouts.cpp +++ b/example/2_simple/timeouts.cpp @@ -49,9 +49,9 @@ void print_employee(mysql::row_view employee) // The main coroutine asio::awaitable coro_main( - std::string server_hostname, - std::string username, - std::string password, + std::string_view server_hostname, + std::string_view username, + std::string_view password, std::string_view company_id ) { @@ -60,12 +60,11 @@ asio::awaitable coro_main( mysql::any_connection conn(co_await asio::this_coro::executor); // The hostname, username, password and database to use - mysql::connect_params params{ - .server_address = mysql::host_and_port(std::move(server_hostname)), - .username = std::move(username), - .password = std::move(password), - .database = "boost_mysql_examples" - }; + 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); From ef5fee49978dcdcfe22d66cc68dcae3830b05658 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:54:41 +0100 Subject: [PATCH 68/77] Workaround for context not running out of work --- test/integration/test/snippets/overview.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index fad3a9d60..975aeaa96 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -31,9 +31,9 @@ #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/snippets_fixture.hpp" namespace mysql = boost::mysql; namespace asio = boost::asio; @@ -234,10 +234,13 @@ asio::awaitable overview_coro(mysql::any_connection& conn) // the operation ends. It's similar to a no-op callback. pool.async_run(asio::detached); //] + + // If we don't use the pool, we may leave unfinished work in the context + co_await pool.async_get_connection(); } } -BOOST_FIXTURE_TEST_CASE(section_overview, snippets_fixture) +BOOST_FIXTURE_TEST_CASE(section_overview, any_connection_fixture) { run_coro(ctx, [&]() { return overview_coro(conn); }); } From a3ff2554378608accc04cff8d04e92436ae005d0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 21:58:17 +0100 Subject: [PATCH 69/77] Use PFR with named entities --- test/integration/test/snippets/overview.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index 975aeaa96..e10b738ed 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -56,8 +56,6 @@ asio::awaitable dont_run(mysql::any_connection& conn) } #endif -namespace { - //[overview_static_struct // This must be placed at namespace scope. // Should contain a member for each field of interest present in our query. @@ -71,6 +69,8 @@ struct employee }; //] +namespace { + asio::awaitable overview_coro(mysql::any_connection& conn) { std::string server_hostname = get_hostname(); From b0d74e63bee77d53db8f483af0974653cf6bb2a2 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:00:36 +0100 Subject: [PATCH 70/77] Update source_sql location --- doc/qbk/00_main.qbk | 2 +- doc/qbk/21_examples.qbk | 9 +++++---- tools/scripts/examples_qbk.py | 6 +----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 24ab430ce..e5230fe48 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -143,8 +143,8 @@ END [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/source_script.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] diff --git a/doc/qbk/21_examples.qbk b/doc/qbk/21_examples.qbk index 12875fa0e..de03d4bc0 100644 --- a/doc/qbk/21_examples.qbk +++ b/doc/qbk/21_examples.qbk @@ -40,6 +40,7 @@ Self-contained programs demonstrating more advanced concepts and techniques. * [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] @@ -272,22 +273,22 @@ This example assumes you have gone through the [link mysql.examples.setup setup] -[section:pipeline (Experimental) Pipelines] +[section:source_script Sourcing a .sql file using multi-queries] This example assumes you have gone through the [link mysql.examples.setup setup]. -[example_pipeline] +[example_source_script] [endsect] -[section:source_script Sourcing a .sql file using multi-queries] +[section:pipeline (Experimental) Pipelines] This example assumes you have gone through the [link mysql.examples.setup setup]. -[example_source_script] +[example_pipeline] [endsect] diff --git a/tools/scripts/examples_qbk.py b/tools/scripts/examples_qbk.py index a03b20d99..470a6cc74 100644 --- a/tools/scripts/examples_qbk.py +++ b/tools/scripts/examples_qbk.py @@ -121,11 +121,11 @@ class MultiExample(NamedTuple): 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 = [ - Example('source_script', '3_advanced/source_script.cpp', 'Sourcing a .sql file using multi-queries'), MultiExample('connection_pool', [ '3_advanced/connection_pool/main.cpp', '3_advanced/connection_pool/types.hpp', @@ -139,10 +139,6 @@ class MultiExample(NamedTuple): ], 'A REST API server that uses connection pooling') ] -# [link mysql.examples.source_script Using multi-queries to source a .sql file] -# [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] ] - ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES def _render_links(examples: List[Example]) -> str: From 03580d74f305e1eead27b58d292848b790f24034 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:01:04 +0100 Subject: [PATCH 71/77] Uncomment reference --- doc/qbk/00_main.qbk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index e5230fe48..ba6e34a6f 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -200,7 +200,7 @@ END -[/ [section:ref Reference] +[section:ref Reference] [xinclude helpers/quickref.xml] [block''''''] [include reference.qbk] @@ -215,4 +215,4 @@ END [include helpers/Stream.qbk] [include helpers/WritableFieldTuple.qbk] [block''''''] -[endsect] ] +[endsect] From 0f9211a3761ce7c182c701f4303102e0f04686dd Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:08:50 +0100 Subject: [PATCH 72/77] Link fixes --- doc/qbk/03_1_tutorial_sync.qbk | 2 +- doc/qbk/03_2_tutorial_async.qbk | 2 +- doc/qbk/03_3_tutorial_with_params.qbk | 4 ++-- doc/qbk/03_4_tutorial_static_interface.qbk | 2 +- doc/qbk/04_overview.qbk | 10 +++++----- doc/qbk/05_connection_establishment.qbk | 4 ++-- doc/qbk/06_sql_formatting.qbk | 12 +++++++----- doc/qbk/13_async.qbk | 9 +++------ doc/qbk/15_sql_formatting_advanced.qbk | 2 +- doc/qbk/helpers/Formattable.qbk | 3 ++- doc/qbk/helpers/quickref.xml | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/qbk/03_1_tutorial_sync.qbk b/doc/qbk/03_1_tutorial_sync.qbk index c8f2944c4..d217a3b03 100644 --- a/doc/qbk/03_1_tutorial_sync.qbk +++ b/doc/qbk/03_1_tutorial_sync.qbk @@ -114,6 +114,6 @@ we are closing the connection, and thus may fail. 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]. +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 index 9fe86ced9..a7f6d11d4 100644 --- a/doc/qbk/03_2_tutorial_async.qbk +++ b/doc/qbk/03_2_tutorial_async.qbk @@ -100,6 +100,6 @@ thread until all the scheduled coroutines and I/O operations complete: 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]. +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 index 866de1490..7a0d84310 100644 --- a/doc/qbk/03_3_tutorial_with_params.qbk +++ b/doc/qbk/03_3_tutorial_with_params.qbk @@ -5,7 +5,7 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) ] -[section:tutorial_async Tutorial 3: queries with parameters] +[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 @@ -143,7 +143,7 @@ With all these changes, this is how our coroutine looks like: 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]. +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 index 428e86888..89245d629 100644 --- a/doc/qbk/03_4_tutorial_static_interface.qbk +++ b/doc/qbk/03_4_tutorial_static_interface.qbk @@ -5,7 +5,7 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) ] -[section:tutorial_async Tutorial 4: the static interface] +[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` diff --git a/doc/qbk/04_overview.qbk b/doc/qbk/04_overview.qbk index 93c7331a4..c83a706ab 100644 --- a/doc/qbk/04_overview.qbk +++ b/doc/qbk/04_overview.qbk @@ -40,8 +40,8 @@ Like most I/O objects, `any_connection` can be constructed from an execution con [tutorial_sync_connection] `any_connection` is named like this for historic reasons: -a [link mysql.templated_connection templated connection class] -came before it. We currently recommend using `any_connection` for new +a templated connection class came before it. +We currently recommend using `any_connection` for new code because it's simpler and more powerful. The MySQL client/server protocol is session-oriented. Before anything else, @@ -131,7 +131,7 @@ without sending them to the server. [section The dynamic and the static interfaces] In MySQL, a ['resultset] refers to the results generated by a SQL query. -A resultset is composed of rows, [link mysql.metadata metadata] and +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. @@ -254,10 +254,10 @@ 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. -Both the sync functions in [link mysql.tutorial.sync the first tutorial] +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 diagnostic] object. +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. diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index 167ed35c5..8436b8c58 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -145,7 +145,7 @@ 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_validation here]. +The full source code for the above example is [link mysql.examples.tls_certificate_verification here]. @@ -202,7 +202,7 @@ 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 -[refmem client_errc max_buffer_size_exceeded] error code. The default size +`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, diff --git a/doc/qbk/06_sql_formatting.qbk b/doc/qbk/06_sql_formatting.qbk index 930a66fe2..b6d6d3678 100644 --- a/doc/qbk/06_sql_formatting.qbk +++ b/doc/qbk/06_sql_formatting.qbk @@ -31,7 +31,7 @@ which can improve efficiency. 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. +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 @@ -57,8 +57,8 @@ 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 +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 @@ -126,12 +126,14 @@ to format MySQL types into a string without creating vulnerabilities, but otherw 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] +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.efficiency efficiency considerations section] for more info. +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: diff --git a/doc/qbk/13_async.qbk b/doc/qbk/13_async.qbk index 65e47b2d3..f408e7d1a 100644 --- a/doc/qbk/13_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/15_sql_formatting_advanced.qbk b/doc/qbk/15_sql_formatting_advanced.qbk index be90d16a3..d46e5a308 100644 --- a/doc/qbk/15_sql_formatting_advanced.qbk +++ b/doc/qbk/15_sql_formatting_advanced.qbk @@ -37,7 +37,7 @@ build query strings incrementally: 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. +[link mysql.sql_formatting_advanced.reference This table] summarizes such types. [endsect] 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 From a0d262311cec4f2bbfc32724a3fe2f6f5de1f43c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:34:36 +0100 Subject: [PATCH 73/77] Remove Boost.Context dependency --- test/integration/CMakeLists.txt | 1 - test/integration/Jamfile | 2 -- 2 files changed, 3 deletions(-) diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index 3d7e1234d..f8fcfbbcc 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -57,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 eb95fdca1..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 From 87719828fb264aa4d8d1bad9aed8b392fba8ffee Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:37:11 +0100 Subject: [PATCH 74/77] inline namespaces to prevent ODR violations --- test/integration/test/snippets/overview.cpp | 2 ++ test/integration/test/snippets/sql_formatting.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index e10b738ed..3fbed8d2b 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -56,6 +56,7 @@ asio::awaitable dont_run(mysql::any_connection& conn) } #endif +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. @@ -68,6 +69,7 @@ struct employee std::string last_name; }; //] +} // namespace overview namespace { 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 { From 1b1f9bf2c7f54a29cba3ff71a06f0dc996215acf Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:56:07 +0100 Subject: [PATCH 75/77] Missing with_diagnostics in coro example --- example/2_simple/coroutines_cpp11.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/2_simple/coroutines_cpp11.cpp b/example/2_simple/coroutines_cpp11.cpp index 3830caa80..63b7d87cb 100644 --- a/example/2_simple/coroutines_cpp11.cpp +++ b/example/2_simple/coroutines_cpp11.cpp @@ -80,7 +80,7 @@ void coro_main( company_id ), result, - yield + mysql::with_diagnostics(yield) ); // Print the employees From acccd57fc13064e69de15606be5d740167a02c17 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 22:56:17 +0100 Subject: [PATCH 76/77] Fix MSVC warning --- test/integration/test/snippets/overview.cpp | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/integration/test/snippets/overview.cpp b/test/integration/test/snippets/overview.cpp index 3fbed8d2b..310353bd8 100644 --- a/test/integration/test/snippets/overview.cpp +++ b/test/integration/test/snippets/overview.cpp @@ -81,18 +81,19 @@ asio::awaitable overview_coro(mysql::any_connection& conn) auto& ctx = static_cast((co_await asio::this_coro::executor).context()); - //[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_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_text_query // Executes 'SELECT 1' and reads the resulting rows into memory From b780376a23aa380915b38af61ee79aabaab17880 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 4 Nov 2024 23:19:07 +0100 Subject: [PATCH 77/77] Workaround old libc++ problem --- example/2_simple/batch_inserts.cpp | 7 ++++--- example/2_simple/batch_inserts_generic.cpp | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/2_simple/batch_inserts.cpp b/example/2_simple/batch_inserts.cpp index 5327dd7d9..ec3d356ef 100644 --- a/example/2_simple/batch_inserts.cpp +++ b/example/2_simple/batch_inserts.cpp @@ -6,6 +6,8 @@ // #include + +#include #ifdef BOOST_ASIO_HAS_CO_AWAIT //[example_batch_inserts @@ -43,7 +45,6 @@ #include #include -#include #include namespace asio = boost::asio; @@ -87,7 +88,7 @@ asio::awaitable coro_main( std::string_view server_hostname, std::string_view username, std::string_view password, - std::span employees + const std::vector& employees ) { // Create a connection. @@ -131,7 +132,7 @@ asio::awaitable coro_main( co_await conn.async_execute( mysql::with_params( "INSERT INTO employee (first_name, last_name, company_id, salary) VALUES {}", - mysql::sequence(employees, format_employee_fn) + mysql::sequence(std::ref(employees), format_employee_fn) ), result ); diff --git a/example/2_simple/batch_inserts_generic.cpp b/example/2_simple/batch_inserts_generic.cpp index 03f01f716..fa8875538 100644 --- a/example/2_simple/batch_inserts_generic.cpp +++ b/example/2_simple/batch_inserts_generic.cpp @@ -49,7 +49,6 @@ #include #include #include -#include #include #include @@ -135,7 +134,7 @@ asio::awaitable coro_main( std::string_view server_hostname, std::string_view username, std::string_view password, - std::span employees + const std::vector& employees ) { // Create a connection. @@ -159,7 +158,7 @@ asio::awaitable coro_main( mysql::with_params( "INSERT INTO employee ({::i}) VALUES {}", get_field_names(), - mysql::sequence(employees, insert_struct_format_fn()) + mysql::sequence(std::ref(employees), insert_struct_format_fn()) ), result );