Added with_params

close #218
This commit is contained in:
Anarthal (Rubén Pérez) 2024-08-01 12:59:56 +02:00 committed by GitHub
parent f76e17c37b
commit a20fc3e5ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 2050 additions and 1214 deletions

View File

@ -8,12 +8,11 @@
[section:sql_formatting (Experimental) Client-side SQL query formatting]
[nochunk]
When issuing queries that contain untrusted input, prepared statement are usually
the way to go. However, some use cases like dynamic filters or batch inserts
are not attainable using them.
The functions described in this section can be used to compose SQL query
strings dynamically client-side while keeping your application secure.
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.
[note
This feature is experimental. Its API may change in subsequent releases.
@ -21,52 +20,23 @@ strings dynamically client-side while keeping your application secure.
[heading Common use cases]
* Queries involving several [*dynamic filters].
See [link mysql.examples.dynamic_filters this example].
* [*Batch inserts]. Inserting rows one by one can lead to poor efficiency.
You can use client-side query formatting to compose a single `INSERT` that
performs several insertions at once. See [link mysql.examples.batch_inserts this example]
for simple batch inserts and [link mysql.examples.batch_inserts_generic this other] for
a generic utility to implement this case.
* [*PATCH-like updates]. If your query must update fields dynamically, based on some input,
prepared statements may not be suitable. See [link mysql.examples.patch_updates this example].
* Queries involving [*dynamic identifiers], like table and field names.
* Performing [*conditional sorting].
* Any other use case that involves SQL that can change dynamically.
This feature can also be used to improve efficiency, as text queries perform less
round-trips to the server. See [link mysql.sql_formatting.efficiency this section]
for more info.
[heading Security considerations]
Client-side SQL formatting [*does protect against SQL injection] by appropriately quoting and escaping
string values. However, this feature [*does not understand your queries]. You need to make sure that
your formatting operations result in valid SQL. If you try to build very complex
queries and make mistakes, attackers may exploit logic errors in your formatting logic to overtake your system.
[heading Formatting simple queries]
[heading Simple queries]
You can use [reflink format_sql] to generate a SQL query from a format string
and a set of parameters, using a notation similar to `std::format`:
[reflink with_params] takes a SQL query
string with placeholders and a set of parameters. When passed to
[refmemunq any_connection execute] or [refmemunq any_connection async_execute],
the query is expanded in the client with the supplied parameters and
sent to the server for execution:
[sql_formatting_simple]
[reflink format_sql] doesn't involve communication with the server.
In order to work, it 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.
Curly braces (`{}`) represent placeholders (technically called ['replacement fields]).
The notation and semantics are similar to [@https://en.cppreference.com/w/cpp/utility/format/format `std::format`].
[note
Getting an `unknown_character_set` error? Have a look at [link mysql.sql_formatting.unknown_character_set this section].
]
All fundamental types can be used with query formatting. This includes integers, floating point types,
All fundamental types can be used as query parameters. This includes integers, floating point types,
strings, blobs, dates and times:
[sql_formatting_other_scalars]
@ -84,7 +54,7 @@ and [link mysql.sql_formatting.reference this table] for a reference of types
that have built-in support for SQL formatting.
[note
Like with `std::format`, the format string passed to `format_sql` must be known at
Like with `std::format`, the query string passed to `with_params` must be known at
compile-time. You can skip this check using the [reflink runtime] function.
]
@ -92,12 +62,106 @@ Like `std::format`, you can use arguments with explicit indices:
[sql_formatting_manual_indices]
You can also use named arguments, using the initializer list overload:
See [link mysql.sql_formatting_advanced.format_string_syntax this section]
for a reference on the format string syntax.
[heading:errors Common errors and how to fix them]
Not all values can be formatted. If the library finds that formatting a certain
value can cause an ambiguity that could lead to a security problem, an error
will be issued and the query won't be sent to the server. Here are the most common errors:
* `client_errc::invalid_encoding`
* Cause: one of your string parameters contains invalid code points.
With the default character set, this means that it contains [*invalid UTF-8].
* Solution: all string values must be encoded according to the connection's character
set (usually UTF-8). Sanitize or reject such values. Use the [reflink blob] and [reflink blob_view]
types for values that don't represent character strings, but arbitrary binary values.
* `client_errc::unformattable_value`
* Cause: one of your parameters contains an invalid value. For instance, a `double`
contains a `NaN` or an `Inf`, unsupported by MySQL.
* Solution: reject such values, or replace them by `NULL` before passing them to client-side SQL formatting.
* `client_errc::unknown_character_set`
* Cause: your connection doesn't know the character set you're using. Knowing the character set in use
is required to generate queries securely. This situation can happen after calling [refmemunq any_connection reset_connection]
or if you used a custom [refmem connect_params connection_collation] when connecting.
* Solution: use a [reflink connection_pool] instead of manually resetting connections. If you can't,
use the default [refmemunq connect_params connection_collation] when connecting, and use
[refmemunq any_connection set_character_set] or [refmemunq any_connection async_set_character_set]
after resetting connections.
* [link mysql.charsets.tracking Learn more] about how character set tracking works.
For example:
[sql_formatting_invalid_encoding]
[heading Formatting queries without executing them]
`with_params` is handy, but may fall short in some cases involving queries with
complex logic. For these cases, you can use [reflink format_sql] and
[reflink format_sql_to] to expand a query without executing it.
These APIs don't involve communication with the server.
[reflink format_sql] is the simplest, and is akin to `std::format`:
[sql_formatting_format_sql]
`format_sql` requires a [reflink format_options] instance describing
connection configuration, like the character set currently in use.
[refmem any_connection format_opts] provides an easy way to retrieve these.
[link mysql.sql_formatting_advanced.format_options This section] contains more info about `format_opts`.
Some use cases, usually involving conditionals, may not be
expressible in terms of a single format string. In such cases, you can
use [reflink format_context] and [reflink format_sql_to] to
build query strings incrementally:
[sql_formatting_incremental_fn]
[sql_formatting_incremental_use]
[reflink sequence] uses this feature to make formatting ranges easier.
Any type that works with `with_params` also does with `format_sql`
and `format_sql_to`. These types are said to satisfy the [reflink Formattable] concept.
[link mysql.sql_formatting.reference This table] summarizes such types.
[heading:ranges Formatting ranges with sequence]
The [reflink sequence] function can be used when the default range formatting isn't sufficient.
If the elements in your range are not formattable, you can pass a user-defined function to `sequence`
describing how to format each element:
[sql_formatting_sequence_1]
By default, elements are separated by commas, but this is configurable:
[sql_formatting_sequence_2]
You can use `sequence` and [reflink with_params] together.
[sql_formatting_named_args]
See [link mysql.sql_formatting_advanced.format_string_syntax this section] for an in-depth
explanation on format strings.
@ -120,84 +184,48 @@ This is equivalent to the previous snippet:
[heading Building SQL strings incrementally using format_sql_to]
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]
[heading:comparison Prepared statements vs. client-side SQL formatting]
Although both serve a similar purpose, they are fundamentally different. Prepared statements
are parsed and expanded by the server. Client-side SQL expands the query in the client
and sends it to the server as a string.
This means that [*client-side SQL does not understand your queries]. It just knows about how
to format MySQL types into a string without creating vulnerabilities, but otherwise treats
your queries as opaque strings. Client-side SQL yields [*greater flexibility] (you can dynamically
compose any query), while statements have more limitations. This also means that
[*you need to pay more attention to compose valid queries], specially when dealing with complex conditionals.
Logic errors may lead to exploits. Please read the [link mysql.sql_formatting.security security considerations section]
for more info.
[heading:ranges Formatting ranges with sequence]
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.
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:
In general, [*use client-side SQL] formatting for the following cases:
[sql_formatting_sequence_1]
By default, elements are separated by commas, but this is configurable:
[sql_formatting_sequence_2]
[heading:unknown_character_set Solving the unknown_character_set error]
If you are getting a `boost::system::system_error` with a
`client_errc::unknown_character_set` error code (or getting this error code by other means),
your connection is currently unaware of the character set it's using, which is required by format operations.
Try the following:
* Make sure that you are performing connection establishment ([refmem any_connection connect] or
[refmemunq any_connection async_connect]) before calling [refmemunq any_connection format_opts].
* Use [refmem any_connection set_character_set] or [refmemunq any_connection async_set_character_set]
to set your connection's character set instead of using raw SQL.
* Some [refmem connect_params connection_collation] values are not supported by all servers
and often trigger fallback behavior. If you are using a non-UTF8 character set, prefer setting it
explicitly using [refmemunq any_connection set_character_set] or [refmemunq any_connection async_set_character_set].
Don't rely on [refmem connect_params connection_collation].
* [refmem any_connection reset_connection] and [refmemunq any_connection async_reset_connection] wipe
character set information. Call [refmemunq any_connection set_character_set] or [refmemunq any_connection async_set_character_set]
after resetting your connection.
[warning
[*Security considerations]: don't craft [reflink format_options] values manually.
Always use [refmem any_connection format_opts].
]
For an explanation on why [reflink format_options] is necessary and how
character set tracking works, please read [link mysql.sql_formatting_advanced.format_options this section].
[heading Solving the invalid_encoding error]
SQL formatting can fail if you provide values that can't be securely formatted.
The most common cause is passing string values that are not valid according to the
passed character set. This triggers a `client_errc::invalid_encoding` error:
[sql_formatting_invalid_encoding]
You can validate your strings beforehand or handle the error once
it happened and reject the input. Other types may also produce format errors.
[tip
If you prefer handling errors with error codes, instead of exceptions,
use [reflink format_sql_to]. Please read
[link mysql.sql_formatting_advanced.error_handling this section] for details.
]
* Simple queries that don't retrieve a lot of data. Default to `with_params` and
only switch to statements if your performance measurements says so.
* Queries involving dynamic SQL that can't be achieved by statements. Typical cases include:
* Dynamic filters ([link mysql.examples.dynamic_filters example]).
* Batch inserts. Inserting rows one by one can lead to poor efficiency.
You can use client-side SQL formatting to compose a single `INSERT` that
inserts several rows at once (see [link mysql.examples.batch_inserts example 1]
and [link mysql.examples.batch_inserts_generic example 2]).
* PATCH-like updates, where the field list in an `UPDATE` must be dynamic
([link mysql.examples.patch_updates example]).
* Queries involving dynamic identifiers, like table and field names.
* Conditional sorting.
* Pipelines consisting of several semicolon-separated queries with dynamic fields.
On the other hand, [*prefer prepared statements] if:
* You are executing the same query over and over. You can prepare the statement
once and execute it several times.
* Your query is retrieving a lot of data, and you have performed the relevant performance measurements.
@ -219,9 +247,55 @@ Both client-side SQL formatting and prepared statements have pros and cons effic
* [*Prepared statements can be re-used]. If you need to execute a query several times,
prepared statements will only be parsed once.
* Client-formatted SQL allows [*more efficient patterns] than prepared statements,
like batch inserts.
* You can use client-formatted SQL to generate several [*semicolon-separated queries]
to be run in batch.
like batch inserts and semicolon-separated queries.
[heading:security Security considerations]
Both client-side SQL formatting and prepared statements [*protect against SQL injection].
Statements do so by parsing the query with placeholders server-side, before performing parameter
substitution. Client-side SQL quotes and escapes your values to avoid injection, but
[*does not understand your queries].
This means that you need to [*ensure that your queries always expand to valid SQL].
This is trivial for simple queries, but may be an issue with more complex ones,
involving ranges or dynamic identifiers. For instance, the following query may
expand to invalid SQL if the provided range is empty:
[sql_formatting_empty_ranges]
The risk is higher if you're building your query by pieces using [reflink format_sql_to].
To sum up:
* Client-side SQL protects against SQL injection.
* Client-side SQL does not protect against logic errors. The risk is only present in complex
queries. We suggest the following advice:
* Avoid complex query generation logic as much as possible.
Use a single format string instead of `format_sql_to`, unless you have no other option.
* When using ranges, consider if the empty range would lead to valid SQL or not.
* Thoroughly test complex query generation logic.
* Client-side SQL requires knowing the connection's current character set. This usually happens
out of the box, and will lead to a [link mysql.sql_formatting.errors controlled error]
otherwise. Some recommendations:
* If in doubt, always use the default character set (`utf8mb4`).
* Never issue `SET NAMES` or `SET CHARACTER SET` statements directly -
use [refmem any_connection set_character_set] or [refmemunq any_connection async_set_character_set], instead.
* If you're using [reflink format_sql] or [reflink format_sql_to], never craft [reflink format_options] values manually.
Use [refmem any_connection format_opts], instead.

View File

@ -15,7 +15,7 @@ You can specialize [reflink formatter] to add formatting support to your types:
[sql_formatting_formatter_specialization]
The type can now be used in [reflink format_sql] and [reflink format_sql_to]:
The type can now be used in [reflink format_sql], [reflink format_sql_to] and [reflink with_params]:
[sql_formatting_formatter_use]
@ -32,6 +32,10 @@ We can now use it like this:
See the [reflink formatter] reference docs for more info.
[heading:format_string_syntax Format string syntax]
This section extends on the supported syntax for format strings.
@ -52,20 +56,10 @@ Indices can appear in any order, and can be repeated:
[sql_formatting_manual_indices]
Finally, you can use named arguments by using the initializer-list overloads,
which creates [reflink format_arg] values:
[sql_formatting_named_args]
Argument names can only contain ASCII letters (lowercase and uppercase),
digits and the underscore character (`_`). Names can't start with a digit.
Format strings can use either manual or automatic indexing, but can't mix them:
[sql_formatting_manual_auto_mix]
Named arguments can be mixed with either manual or automatic indexing.
Unreferenced format arguments are ignored. It's not an error to supply more
format arguments than required:
@ -79,8 +73,8 @@ Format specifiers (e.g. `{:i}`) are supported for some types,
but are far less common than in fmtlib, since most types have a
single, canonical representation.
Specifiers can appear when doing automatic indexing (e.g. `{:i}`),
manual indexing (e.g. `{0:i}`) and named arguments (e.g. `{name:i}`).
Specifiers can appear when doing automatic indexing (e.g. `{:i}`) or
manual indexing (e.g. `{0:i}`).
Types specializing formatters can define custom specifiers.
Only printable ASCII characters that are not `{` or `}` can be used as specifiers.
@ -99,6 +93,10 @@ Some values can't be securely formatted. For instance, C++
Strings can contain byte sequences that don't represent valid characters,
which makes them impossible to escape securely.
When using [reflink with_params] and any of these errors is encountered,
the [refmemunq any_connection execute] operation fails, as if a server error
had been encountered. This is transparent to the user, so no action is required.
[reflink format_sql] reports these errors by throwing `boost::system::system_error` exceptions,
which contain an error code with details about what happened. For instance:
@ -144,18 +142,24 @@ that formatting functions need to know in order to work:
[link mysql.error_handling.system_result `boost::system::result`]`<`[reflink format_options]`>`.
If the connection could not determine the current character set, the result will contain an error.
For a reference on how character set tracking works, please read [link mysql.charsets.tracking this section].
[note
Prior to connection establishment, the connection's character set is always unknown.
Connect your connection before calling `format_opts`.
]
[warning
Passing an incorrect `format_options` value to formatting functions may cause
escaping to generate incorrect values, which may generate vulnerabilities.
Stay safe and always use [refmem any_connection format_opts] instead of
hand-crafting `format_options` values. Doing this, if the character set can't be safely
determined, you will get a [link mysql.sql_formatting.unknown_character_set `client_errc::unknown_character_set`]
error instead of a vulnerability.
determined, you will get a `client_errc::unknown_character_set` error instead of a vulnerability.
]
[heading Custom string types]
[reflink format_sql_to] can be used with string types that are not `std::string`,

View File

@ -8,8 +8,9 @@
[section:boost__mysql__ExecutionRequest ExecutionRequest concept]
An execution request represents a SQL statement to be executed by the
server, plus any parameters required to run the query. It may model
a plain text query, or a prepared statement handle with bound parameters.
server, plus any required parameters. It may model
a plain text query, a client-side formatted query with parameters,
or a prepared statement handle with parameters.
Formally, a type `T` is a `ExecutionRequest` if it fulfills any of the following:
@ -19,6 +20,8 @@ Formally, a type `T` is a `ExecutionRequest` if it fulfills any of the following
reference to it.
* An instantiation of the [reflink bound_statement_iterator_range] class, or a (possibly cv-qualified)
reference to it.
* An instantiation of the [reflink with_params_t] class, or a (possibly cv-qualified)
reference to it.
This definition may be extended in future versions, but the above types will still satisfy `ExecutionRequest`.

View File

@ -62,6 +62,7 @@
<member><link linkend="mysql.ref.boost__mysql__static_execution_state">static_execution_state</link></member>
<member><link linkend="mysql.ref.boost__mysql__static_results">static_results</link></member>
<member><link linkend="mysql.ref.boost__mysql__unix_path">unix_path</link></member>
<member><link linkend="mysql.ref.boost__mysql__with_params_t">with_params_t</link></member>
</simplelist>
</entry>
<entry valign="top">
@ -103,6 +104,7 @@
<member><link linkend="mysql.ref.boost__mysql__runtime">runtime</link></member>
<member><link linkend="mysql.ref.boost__mysql__sequence">sequence</link></member>
<member><link linkend="mysql.ref.boost__mysql__throw_on_error">throw_on_error</link></member>
<member><link linkend="mysql.ref.boost__mysql__with_params">with_params</link></member>
</simplelist>
</entry>
<entry valign="top">
@ -113,6 +115,7 @@
<member><link linkend="mysql.ref.boost__mysql__days">days</link></member>
<member><link linkend="mysql.ref.boost__mysql__error_code">error_code</link></member>
<member><link linkend="mysql.ref.boost__mysql__format_context">format_context</link></member>
<member><link linkend="mysql.ref.boost__mysql__make_tuple_element_t">make_tuple_element_t</link></member>
<member><link linkend="mysql.ref.boost__mysql__metadata_collection_view">metadata_collection_view</link></member>
<member><link linkend="mysql.ref.boost__mysql__string_view">string_view</link></member>
<member><link linkend="mysql.ref.boost__mysql__tcp_connection">tcp_connection</link></member>

View File

@ -22,16 +22,11 @@
// Note: client-side SQL formatting is an experimental feature.
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/span.hpp>
#include <boost/describe/class.hpp>
#include <boost/describe/members.hpp>
#include <boost/describe/modifiers.hpp>
#include <boost/json/parse.hpp>
@ -41,9 +36,6 @@
#include <iostream>
#include <string>
using boost::mysql::error_code;
using boost::mysql::string_view;
/**
* We will use Boost.Describe to easily parse the JSON file
* into a std::vector<employee>. The JSON file contain an array
@ -75,50 +67,6 @@ static std::string read_file(const char* file_name)
return std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// Composes an INSERT SQL query suitable to be sent to the server.
// For instance, 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)
static std::string compose_batch_insert(
// Connection config options required for the formatting.
// This includes the character set currently in use.
boost::mysql::format_options opts,
// The list of employees to insert, as read from the JSON file
const std::vector<employee>& employees
)
{
// We need at least one employee to insert
assert(!employees.empty());
// 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) {
// 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.
// Doubles are formatted as number literals.
boost::mysql::format_sql_to(
ctx,
"({}, {}, {}, {})",
emp.first_name,
emp.last_name,
emp.company_id,
emp.salary
);
};
// sequence() takes a range and a formatter function.
// It will call the formatter function for each object in the sequence,
// adding commas between invocations.
return boost::mysql::format_sql(
opts,
"INSERT INTO employee (first_name, last_name, company_id, salary) VALUES {}",
boost::mysql::sequence(employees, format_employee_fn)
);
}
void main_impl(int argc, char** argv)
{
if (argc != 5)
@ -156,19 +104,42 @@ void main_impl(int argc, char** argv)
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;
// Connect to the server
conn.connect(params);
// Compose the query. format_opts() returns a system::result<format_options>,
// 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 set_character_set if this happens.
std::string query = compose_batch_insert(conn.format_opts().value(), values);
// 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) {
// 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(
ctx,
"({}, {}, {}, {})",
emp.first_name,
emp.last_name,
emp.company_id,
emp.salary
);
};
// Execute the query as usual. Note that, unlike with prepared statements,
// formatting happened in the client, and not in the server.
boost::mysql::results result;
conn.execute(query, result);
// Compose and execute the batch INSERT. When passed to execute(), with_params
// replaces placeholders ({}) by actual parameter values before sending the query to the server.
// 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(
"INSERT INTO employee (first_name, last_name, company_id, salary) VALUES {}",
boost::mysql::sequence(values, format_employee_fn)
),
result
);
std::cout << "Done\n";
// Notify the MySQL server we want to quit, then close the underlying connection.

View File

@ -5,16 +5,7 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/describe/class.hpp>
#include <boost/mp11/list.hpp>
#include <boost/mp11/tuple.hpp>
#include <array>
#include <cstddef>
#ifdef BOOST_DESCRIBE_CXX14
@ -31,25 +22,27 @@
// Note: client-side SQL formatting is an experimental feature.
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/span.hpp>
#include <boost/describe/class.hpp>
#include <boost/describe/members.hpp>
#include <boost/describe/modifiers.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value_to.hpp>
#include <boost/mp11/list.hpp>
#include <boost/mp11/tuple.hpp>
#include <array>
#include <cstddef>
#include <fstream>
#include <iostream>
#include <string>
using boost::mysql::error_code;
using boost::mysql::string_view;
namespace describe = boost::describe;
namespace mp11 = boost::mp11;
@ -165,21 +158,17 @@ void main_impl(int argc, char** argv)
exit(1);
}
// Compose the query. We use sequence() to format C++ ranges as
// comma-separated sequences.
// Recall that format_opts() returns a system::result<format_options>,
// which can contain an error if the connection doesn't know which character set is using.
// Use set_character_set if this happens.
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"INSERT INTO employee ({::i}) VALUES {}",
get_field_names<employee>(),
boost::mysql::sequence(values, insert_struct_format_fn())
);
// Execute the query as usual.
// 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(query, result);
conn.execute(
boost::mysql::with_params(
"INSERT INTO employee ({::i}) VALUES {}",
get_field_names<employee>(),
boost::mysql::sequence(values, insert_struct_format_fn())
),
result
);
std::cout << "Done\n";
// Notify the MySQL server we want to quit, then close the underlying connection.

View File

@ -20,6 +20,7 @@
#include <boost/mysql/static_results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/throw_on_error.hpp>
#include <boost/mysql/with_params.hpp>
#include <iterator>
#include <tuple>
@ -71,6 +72,9 @@ std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
std::make_move_iterator(result.rows().begin()),
std::make_move_iterator(result.rows().end())
);
// If an exception is thrown, pooled_connection's destructor will
// return the connection automatically to the pool.
}
optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield)
@ -84,31 +88,25 @@ optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yi
mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// Our query has a parameter, so we need a prepared statement.
// We don't need to deallocate this statement explicitly
// (no need to call any_connection::async_close_statement).
// The connection pool takes care of this for us
// (by using any_connection::async_reset_connection).
mysql::statement stmt = conn->async_prepare_statement(
"SELECT id, title, content FROM notes WHERE id = ?",
// When executed, with_params expands a query client-side before sending it to the server.
// Placeholders are marked with {}
mysql::static_results<note_t> result;
conn->async_execute(
mysql::with_params("SELECT id, title, content FROM notes WHERE id = {}", note_id),
result,
diag,
yield[ec]
);
mysql::throw_on_error(ec, diag);
// Execute the statement. We use the static interface to parse results
mysql::static_results<note_t> result;
conn->async_execute(stmt.bind(note_id), result, diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// We did nothing to mutate session state, so we can skip reset
conn.return_without_reset();
// An empty results object indicates that no note was found
if (result.rows().empty())
return {};
else
return std::move(result.rows()[0]);
// There's no need to return the connection explicitly to the pool,
// pooled_connection's destructor takes care of it.
}
note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield)
@ -122,9 +120,10 @@ note_t note_repository::create_note(string_view title, string_view content, boos
mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// Our query has parameters, so we need to prepare a statement.
// As explained above, there is no need to deallocate the statement explicitly,
// since the pool takes care of it.
// We will use statements in this function for the sake of example.
// We don't need to deallocate the statement explicitly,
// since the pool takes care of it after the connection is returned.
// You can also use with_params instead of statements.
mysql::statement stmt = conn->async_prepare_statement(
"INSERT INTO notes (title, content) VALUES (?, ?)",
diag,
@ -164,30 +163,30 @@ optional<note_t> note_repository::replace_note(
mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// Our query has parameters, so we need to prepare a statement.
// As explained above, there is no need to deallocate the statement explicitly,
// since the pool takes care of it.
mysql::statement stmt = conn->async_prepare_statement(
"UPDATE notes SET title = ?, content = ? WHERE id = ?",
// Expand and execute the query.
// It won't produce any rows, so we can use static_results<std::tuple<>>
mysql::static_results<std::tuple<>> empty_result;
conn->async_execute(
mysql::with_params(
"UPDATE notes SET title = {}, content = {} WHERE id = {}",
title,
content,
note_id
),
empty_result,
diag,
yield[ec]
);
mysql::throw_on_error(ec, diag);
// Execute the statement. The statement won't produce any rows,
// so we can use static_results<std::tuple<>>
mysql::static_results<std::tuple<>> empty_result;
conn->async_execute(stmt.bind(title, content, note_id), empty_result, diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// We didn't mutate session state, so we can skip reset
conn.return_without_reset();
// No affected rows means that the note doesn't exist
if (empty_result.affected_rows() == 0u)
return {};
return note_t{note_id, title, content};
// There's no need to return the connection explicitly to the pool,
// pooled_connection's destructor takes care of it.
}
bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield)
@ -201,23 +200,22 @@ bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_conte
mysql::pooled_connection conn = pool_.async_get_connection(diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// Our query has parameters, so we need to prepare a statement.
// As explained above, there is no need to deallocate the statement explicitly,
// since the pool takes care of it.
mysql::statement stmt = conn->async_prepare_statement("DELETE FROM notes WHERE id = ?", diag, yield[ec]);
// Expand and execute the query.
// It won't produce any rows, so we can use static_results<std::tuple<>>
mysql::static_results<std::tuple<>> empty_result;
conn->async_execute(
mysql::with_params("DELETE FROM notes WHERE id = {}", note_id),
empty_result,
diag,
yield[ec]
);
mysql::throw_on_error(ec, diag);
// Execute the statement. The statement won't produce any rows,
// so we can use static_results<std::tuple<>>
mysql::static_results<std::tuple<>> empty_result;
conn->async_execute(stmt.bind(note_id), empty_result, diag, yield[ec]);
mysql::throw_on_error(ec, diag);
// We didn't mutate session state, so we can skip reset
conn.return_without_reset();
// No affected rows means that the note didn't exist
return empty_result.affected_rows() != 0u;
// There's no need to return the connection explicitly to the pool,
// pooled_connection's destructor takes care of it.
}
//]

View File

@ -15,7 +15,6 @@
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
@ -29,7 +28,6 @@
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/core/span.hpp>
#include <boost/optional/optional.hpp>
#include <cassert>
@ -188,6 +186,12 @@ static cmdline_args parse_cmdline_args(int argc, char** argv)
}
// Composes a SELECT query to retrieve employees according to the passed filters.
// We allow an optional ORDER BY clause that must be added dynamically,
// so we can't express our query as a single format string.
// This function uses format_sql_to to build a query string incrementally.
// format_sql_to requires us to pass a format_options value, containing configuration
// 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,
const std::vector<filter>& filts,
@ -228,7 +232,10 @@ std::string compose_get_employees_query(
boost::mysql::format_sql_to(ctx, " ORDER BY {:i}", *order_by);
}
// Get our generated query
// Get our generated query. get() returns a system::result<std::string>, which
// will contain errors if any of the args couldn't be formatted. This can happen
// if you pass string values containing invalid UTF-8.
// value() will throw an exception if that's the case.
return std::move(ctx).get().value();
}
@ -282,8 +289,9 @@ void main_impl(int argc, char** argv)
// 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(query, result, diag, yield[ec]);
conn.async_execute(string_view(query), result, diag, yield[ec]);
boost::mysql::throw_on_error(ec, diag);
// Print the employees

View File

@ -16,23 +16,18 @@
// Note: client-side SQL formatting is an experimental feature.
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/ssl_mode.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/span.hpp>
#include <cstdint>
#include <iostream>
#include <string>
using boost::mysql::error_code;
using boost::mysql::field_view;
using boost::mysql::string_view;
@ -170,30 +165,32 @@ void main_impl(int argc, char** argv)
// 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 the query. We use sequence() to output the update list separated by commas.
// 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.
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"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
);
// Execute the query as usual.
boost::mysql::results result;
conn.execute(query, 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).

View File

@ -66,5 +66,6 @@
#include <boost/mysql/underlying_row.hpp>
#include <boost/mysql/unix.hpp>
#include <boost/mysql/unix_ssl.hpp>
#include <boost/mysql/with_params.hpp>
#endif

View File

@ -144,12 +144,8 @@ class any_connection
static std::unique_ptr<detail::engine> create_engine(asio::any_io_executor ex, asio::ssl::context* ctx);
// Used by tests
any_connection(
std::size_t initial_buffer_size,
std::size_t max_buffer_size,
std::unique_ptr<detail::engine> eng
)
: impl_(initial_buffer_size, max_buffer_size, std::move(eng))
any_connection(std::unique_ptr<detail::engine> eng, any_connection_params params)
: impl_(params.initial_buffer_size, params.max_buffer_size, std::move(eng))
{
}
@ -164,11 +160,7 @@ public:
* an \ref any_connection_params object to this constructor.
*/
any_connection(boost::asio::any_io_executor ex, any_connection_params params = {})
: any_connection(
params.initial_buffer_size,
params.max_buffer_size,
create_engine(std::move(ex), params.ssl_context)
)
: any_connection(create_engine(std::move(ex), params.ssl_context), params)
{
}
@ -408,18 +400,18 @@ public:
/// \copydoc connection::execute
template <BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest, BOOST_MYSQL_RESULTS_TYPE ResultsType>
void execute(const ExecutionRequest& req, ResultsType& result, error_code& err, diagnostics& diag)
void execute(ExecutionRequest&& req, ResultsType& result, error_code& err, diagnostics& diag)
{
impl_.execute(req, result, err, diag);
impl_.execute(std::forward<ExecutionRequest>(req), result, err, diag);
}
/// \copydoc execute
template <BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest, BOOST_MYSQL_RESULTS_TYPE ResultsType>
void execute(const ExecutionRequest& req, ResultsType& result)
void execute(ExecutionRequest&& req, ResultsType& result)
{
error_code err;
diagnostics diag;
execute(req, result, err, diag);
execute(std::forward<ExecutionRequest>(req), result, err, diag);
detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION);
}
@ -463,25 +455,20 @@ public:
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
BOOST_MYSQL_EXECUTION_STATE_TYPE ExecutionStateType>
void start_execution(
const ExecutionRequest& req,
ExecutionStateType& st,
error_code& err,
diagnostics& diag
)
void start_execution(ExecutionRequest&& req, ExecutionStateType& st, error_code& err, diagnostics& diag)
{
impl_.start_execution(req, st, err, diag);
impl_.start_execution(std::forward<ExecutionRequest>(req), st, err, diag);
}
/// \copydoc start_execution
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
BOOST_MYSQL_EXECUTION_STATE_TYPE ExecutionStateType>
void start_execution(const ExecutionRequest& req, ExecutionStateType& st)
void start_execution(ExecutionRequest&& req, ExecutionStateType& st)
{
error_code err;
diagnostics diag;
start_execution(req, st, err, diag);
start_execution(std::forward<ExecutionRequest>(req), st, err, diag);
detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION);
}

View File

@ -362,18 +362,18 @@ public:
* Metadata in `result` will be populated according to `this->meta_mode()`.
*/
template <BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest, BOOST_MYSQL_RESULTS_TYPE ResultsType>
void execute(const ExecutionRequest& req, ResultsType& result, error_code& err, diagnostics& diag)
void execute(ExecutionRequest&& req, ResultsType& result, error_code& err, diagnostics& diag)
{
impl_.execute(req, result, err, diag);
impl_.execute(std::forward<ExecutionRequest>(req), result, err, diag);
}
/// \copydoc execute
template <BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest, BOOST_MYSQL_RESULTS_TYPE ResultsType>
void execute(const ExecutionRequest& req, ResultsType& result)
void execute(ExecutionRequest&& req, ResultsType& result)
{
error_code err;
diagnostics diag;
execute(req, result, err, diag);
execute(std::forward<ExecutionRequest>(req), result, err, diag);
detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION);
}
@ -461,25 +461,20 @@ public:
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
BOOST_MYSQL_EXECUTION_STATE_TYPE ExecutionStateType>
void start_execution(
const ExecutionRequest& req,
ExecutionStateType& st,
error_code& err,
diagnostics& diag
)
void start_execution(ExecutionRequest&& req, ExecutionStateType& st, error_code& err, diagnostics& diag)
{
impl_.start_execution(req, st, err, diag);
impl_.start_execution(std::forward<ExecutionRequest>(req), st, err, diag);
}
/// \copydoc start_execution
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
BOOST_MYSQL_EXECUTION_STATE_TYPE ExecutionStateType>
void start_execution(const ExecutionRequest& req, ExecutionStateType& st)
void start_execution(ExecutionRequest&& req, ExecutionStateType& st)
{
error_code err;
diagnostics diag;
start_execution(req, st, err, diag);
start_execution(std::forward<ExecutionRequest>(req), st, err, diag);
detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION);
}

View File

@ -8,37 +8,75 @@
#ifndef BOOST_MYSQL_DETAIL_ANY_EXECUTION_REQUEST_HPP
#define BOOST_MYSQL_DETAIL_ANY_EXECUTION_REQUEST_HPP
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/statement.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/core/span.hpp>
#include <cstdint>
#include <type_traits>
#include <vector>
namespace boost {
namespace mysql {
class field_view;
class format_arg;
namespace detail {
struct any_execution_request
{
enum class type_t
{
query,
query_with_params,
stmt
};
union data_t
{
string_view query;
struct
struct query_with_params_t
{
statement stmt;
constant_string_view query;
span<const format_arg> args;
} query_with_params;
struct stmt_t
{
std::uint32_t stmt_id;
std::uint16_t num_params;
span<const field_view> params;
} stmt;
data_t(string_view q) noexcept : query(q) {}
data_t(statement s, span<const field_view> params) noexcept : stmt{s, params} {}
} data;
bool is_query;
data_t(query_with_params_t v) noexcept : query_with_params(v) {}
data_t(stmt_t v) noexcept : stmt(v) {}
};
any_execution_request(string_view q) noexcept : data(q), is_query(true) {}
any_execution_request(statement s, span<const field_view> params) noexcept
: data(s, params), is_query(false)
type_t type;
data_t data;
any_execution_request(string_view q) noexcept : type(type_t::query), data(q) {}
any_execution_request(data_t::query_with_params_t v) noexcept : type(type_t::query_with_params), data(v)
{
}
any_execution_request(data_t::stmt_t v) noexcept : type(type_t::stmt), data(v) {}
};
struct no_execution_request_traits
{
};
template <class T, class = void>
struct execution_request_traits : no_execution_request_traits
{
};
template <class T>
struct execution_request_traits<T, typename std::enable_if<std::is_convertible<T, string_view>::value>::type>
{
static any_execution_request make_request(string_view input, std::vector<field_view>&) { return input; }
};
} // namespace detail

View File

@ -22,23 +22,19 @@
#include <boost/mysql/detail/access.hpp>
#include <boost/mysql/detail/algo_params.hpp>
#include <boost/mysql/detail/any_execution_request.hpp>
#include <boost/mysql/detail/config.hpp>
#include <boost/mysql/detail/connect_params_helpers.hpp>
#include <boost/mysql/detail/engine.hpp>
#include <boost/mysql/detail/execution_processor/execution_processor.hpp>
#include <boost/mysql/detail/writable_field_traits.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/mp11/integer_sequence.hpp>
#include <boost/system/result.hpp>
#include <array>
#include <cstddef>
#include <cstring>
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace boost {
namespace mysql {
@ -52,12 +48,11 @@ class pipeline_request;
namespace detail {
// Forward decl
class connection_state;
//
// Helpers to interact with connection_state, without including its definition
//
class connection_state;
struct connection_state_deleter
{
BOOST_MYSQL_DECL void operator()(connection_state*) const;
@ -72,71 +67,9 @@ any_resumable_ref setup(connection_state&, diagnostics&, const AlgoParams&);
template <class AlgoParams>
typename AlgoParams::result_type get_result(const connection_state&);
//
// execution helpers
//
template <class... T, std::size_t... I>
std::array<field_view, sizeof...(T)> tuple_to_array_impl(const std::tuple<T...>& t, mp11::index_sequence<I...>) noexcept
{
boost::ignore_unused(t); // MSVC gets confused if sizeof...(T) == 0
return std::array<field_view, sizeof...(T)>{{to_field(std::get<I>(t))...}};
}
template <class... T>
std::array<field_view, sizeof...(T)> tuple_to_array(const std::tuple<T...>& t) noexcept
{
return tuple_to_array_impl(t, mp11::make_index_sequence<sizeof...(T)>());
}
struct query_request_getter
{
any_execution_request value;
any_execution_request get() const noexcept { return value; }
};
inline query_request_getter make_request_getter(string_view q, std::vector<field_view>&) noexcept
{
return query_request_getter{q};
}
struct stmt_it_request_getter
{
statement stmt;
span<const field_view> params; // Points into the connection state's shared fields
any_execution_request get() const noexcept { return any_execution_request(stmt, params); }
};
template <class FieldViewFwdIterator>
inline stmt_it_request_getter make_request_getter(
const bound_statement_iterator_range<FieldViewFwdIterator>& req,
std::vector<field_view>& shared_fields
)
{
auto& impl = access::get_impl(req);
shared_fields.assign(impl.first, impl.last);
return {impl.stmt, shared_fields};
}
template <std::size_t N>
struct stmt_tuple_request_getter
{
statement stmt;
std::array<field_view, N> params;
any_execution_request get() const noexcept { return any_execution_request(stmt, params); }
};
template <class WritableFieldTuple>
stmt_tuple_request_getter<std::tuple_size<WritableFieldTuple>::value>
make_request_getter(const bound_statement_tuple<WritableFieldTuple>& req, std::vector<field_view>&)
{
auto& impl = access::get_impl(req);
return {impl.stmt, tuple_to_array(impl.params)};
}
//
// helpers to run algos
//
template <class AlgoParams>
using has_void_result = std::is_same<typename AlgoParams::result_type, void>;
@ -185,16 +118,27 @@ struct generic_algo_handler
connection_state* st;
};
// Note: async_initiate args be, at least:
// 1. a diagnostics*
// 2. a (possibly smart) pointer to an I/O object
// 3. everything else
// This uniform structure allows to write completion tokens with extra functionality
// Note: the 1st async_initiate arg should be a diagnostics*,
// so completion tokens knowing how Boost.MySQL work can operate
class connection_impl
{
std::unique_ptr<engine> engine_;
std::unique_ptr<connection_state, connection_state_deleter> st_;
// Helper for execution requests
template <class T>
static auto make_request(T&& input, connection_state& st)
-> decltype(execution_request_traits<typename std::decay<T>::type>::make_request(
std::forward<T>(input),
get_shared_fields(st)
))
{
return execution_request_traits<typename std::decay<T>::type>::make_request(
std::forward<T>(input),
get_shared_fields(st)
);
}
// Generic algorithm
template <class AlgoParams>
typename AlgoParams::result_type run_impl(
@ -330,15 +274,14 @@ class connection_impl
diagnostics* diag,
engine* eng,
connection_state* st,
const ExecutionRequest& req,
ExecutionRequest&& req,
execution_processor* proc
)
{
auto getter = make_request_getter(req, get_shared_fields(*st));
async_run_impl(
*eng,
*st,
execute_algo_params{getter.get(), proc},
execute_algo_params{make_request(std::forward<ExecutionRequest>(req), *st), proc},
*diag,
std::forward<Handler>(handler)
);
@ -354,15 +297,14 @@ class connection_impl
diagnostics* diag,
engine* eng,
connection_state* st,
const ExecutionRequest& req,
ExecutionRequest&& req,
execution_processor* proc
)
{
auto getter = make_request_getter(req, get_shared_fields(*st));
async_run_impl(
*eng,
*st,
start_execution_algo_params{getter.get(), proc},
start_execution_algo_params{make_request(std::forward<ExecutionRequest>(req), *st), proc},
*diag,
std::forward<Handler>(handler)
);
@ -499,10 +441,16 @@ public:
// Execute
template <class ExecutionRequest, class ResultsType>
void execute(const ExecutionRequest& req, ResultsType& result, error_code& err, diagnostics& diag)
void execute(ExecutionRequest&& req, ResultsType& result, error_code& err, diagnostics& diag)
{
auto getter = make_request_getter(req, get_shared_fields(*st_));
run(execute_algo_params{getter.get(), &access::get_impl(result).get_interface()}, err, diag);
run(
execute_algo_params{
make_request(std::forward<ExecutionRequest>(req), *st_),
&access::get_impl(result).get_interface()
},
err,
diag
);
}
template <class ExecutionRequest, class ResultsType, class CompletionToken>
@ -536,14 +484,20 @@ public:
// Start execution
template <class ExecutionRequest, class ExecutionStateType>
void start_execution(
const ExecutionRequest& req,
ExecutionRequest&& req,
ExecutionStateType& exec_st,
error_code& err,
diagnostics& diag
)
{
auto getter = make_request_getter(req, get_shared_fields(*st_));
run(start_execution_algo_params{getter.get(), &access::get_impl(exec_st).get_interface()}, err, diag);
run(
start_execution_algo_params{
make_request(std::forward<ExecutionRequest>(req), *st_),
&access::get_impl(exec_st).get_interface()
},
err,
diag
);
}
template <class ExecutionRequest, class ExecutionStateType, class CompletionToken>

View File

@ -8,9 +8,9 @@
#ifndef BOOST_MYSQL_DETAIL_EXECUTION_CONCEPTS_HPP
#define BOOST_MYSQL_DETAIL_EXECUTION_CONCEPTS_HPP
#include <boost/mysql/statement.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/detail/any_execution_request.hpp>
#include <boost/mysql/detail/config.hpp>
#include <type_traits>
@ -61,33 +61,12 @@ template <class T>
concept results_type = std::is_same_v<T, results> || is_static_results<T>::value;
// Execution request
template <class T>
struct is_bound_statement_tuple : std::false_type
{
};
template <class T>
struct is_bound_statement_tuple<bound_statement_tuple<T>> : std::true_type
{
};
template <class T>
struct is_bound_statement_range : std::false_type
{
};
template <class T>
struct is_bound_statement_range<bound_statement_iterator_range<T>> : std::true_type
{
};
template <class T>
struct is_execution_request
{
using without_cvref = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
static constexpr bool value = std::is_convertible<T, string_view>::value ||
is_bound_statement_tuple<without_cvref>::value ||
is_bound_statement_range<without_cvref>::value;
static constexpr bool value = !std::is_base_of<
no_execution_request_traits,
execution_request_traits<typename std::decay<T>::type>>::value;
};
template <class T>

View File

@ -8,6 +8,7 @@
#ifndef BOOST_MYSQL_DETAIL_FORMAT_SQL_HPP
#define BOOST_MYSQL_DETAIL_FORMAT_SQL_HPP
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/string_view.hpp>
@ -30,6 +31,7 @@ struct formatter;
class format_context_base;
class formattable_ref;
class format_arg;
namespace detail {
@ -157,6 +159,9 @@ struct formattable_ref_impl
template <class T>
formattable_ref_impl make_formattable_ref(T&& v);
BOOST_MYSQL_DECL
void vformat_sql_to(format_context_base& ctx, constant_string_view format_str, span<const format_arg> args);
} // namespace detail
} // namespace mysql
} // namespace boost

View File

@ -555,7 +555,14 @@ struct formatter<format_sequence_view<It, Sentinel, FormatFn>>
* in `args` (there aren't enough arguments or a named argument is not found).
*/
template <BOOST_MYSQL_FORMATTABLE... Formattable>
void format_sql_to(format_context_base& ctx, constant_string_view format_str, Formattable&&... args);
void format_sql_to(format_context_base& ctx, constant_string_view format_str, Formattable&&... args)
{
std::initializer_list<format_arg> args_il{
{string_view(), std::forward<Formattable>(args)}
...
};
detail::vformat_sql_to(ctx, format_str, args_il);
}
/**
* \copydoc format_sql_to
@ -563,12 +570,14 @@ void format_sql_to(format_context_base& ctx, constant_string_view format_str, Fo
* \n
* This overload allows using named arguments.
*/
BOOST_MYSQL_DECL
void format_sql_to(
inline void format_sql_to(
format_context_base& ctx,
constant_string_view format_str,
std::initializer_list<format_arg> args
);
)
{
detail::vformat_sql_to(ctx, format_str, args);
}
/**
* \brief (EXPERIMENTAL) Composes a SQL query client-side.

View File

@ -158,20 +158,6 @@ boost::mysql::detail::formattable_ref_impl boost::mysql::detail::make_formattabl
return make_formattable_ref_writable(std::forward<T>(v), is_writable_field_ref<T>());
}
template <BOOST_MYSQL_FORMATTABLE... Formattable>
void boost::mysql::format_sql_to(
format_context_base& ctx,
constant_string_view format_str,
Formattable&&... args
)
{
std::initializer_list<format_arg> args_il{
{string_view(), std::forward<Formattable>(args)}
...
};
format_sql_to(ctx, format_str, args_il);
}
template <BOOST_MYSQL_FORMATTABLE... Formattable>
std::string boost::mysql::format_sql(
format_options opts,

View File

@ -545,13 +545,13 @@ void boost::mysql::format_context_base::format_arg(detail::formattable_ref_impl
}
}
void boost::mysql::format_sql_to(
void boost::mysql::detail::vformat_sql_to(
format_context_base& ctx,
constant_string_view format_str,
std::initializer_list<format_arg> args
span<const format_arg> args
)
{
detail::format_state(ctx, {args.begin(), args.end()}).format(format_str.get());
detail::format_state(ctx, args).format(format_str.get());
}
std::string boost::mysql::format_sql(

View File

@ -54,19 +54,13 @@ class serialization_context
void append_to_buffer(span<const std::uint8_t> contents)
{
// Do nothing if we previously encountered an error
if (err_)
return;
// Check if the buffer has space for the given contents
if (buffer_.size() + contents.size() > max_buffer_size_)
{
err_ = client_errc::max_buffer_size_exceeded;
return;
}
add_error(client_errc::max_buffer_size_exceeded);
// Copy
buffer_.insert(buffer_.end(), contents.begin(), contents.end());
// Copy if there was no error
if (!err_)
buffer_.insert(buffer_.end(), contents.begin(), contents.end());
}
void append_header() { append_to_buffer(std::array<std::uint8_t, frame_header_size>{}); }
@ -138,6 +132,19 @@ public:
// To be called by serialize() functions. Appends bytes to the buffer.
void add(span<const std::uint8_t> content) { add_impl(content); }
// Make serialization_context compatible with output_string
void append(const char* content, std::size_t size)
{
add({reinterpret_cast<const std::uint8_t*>(content), size});
}
// Sets the error state
void add_error(error_code ec)
{
if (!err_)
err_ = ec;
}
error_code error() const { return err_; }
// Write frame headers to an already serialized message with space for them

View File

@ -8,35 +8,54 @@
#ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_START_EXECUTION_HPP
#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_START_EXECUTION_HPP
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/detail/algo_params.hpp>
#include <boost/mysql/detail/any_execution_request.hpp>
#include <boost/mysql/detail/execution_processor/execution_processor.hpp>
#include <boost/mysql/detail/next_action.hpp>
#include <boost/mysql/detail/output_string.hpp>
#include <boost/mysql/detail/resultset_encoding.hpp>
#include <boost/mysql/impl/internal/coroutine.hpp>
#include <boost/mysql/impl/internal/protocol/impl/serialization_context.hpp>
#include <boost/mysql/impl/internal/protocol/serialization.hpp>
#include <boost/mysql/impl/internal/sansio/connection_state_data.hpp>
#include <boost/mysql/impl/internal/sansio/read_resultset_head.hpp>
#include <boost/core/span.hpp>
namespace boost {
namespace mysql {
namespace detail {
inline error_code check_client_errors(const any_execution_request& req)
// A serializable to generate the query in the write buffer without copies
struct query_with_params
{
if (req.is_query)
return error_code();
return req.data.stmt.stmt.num_params() == req.data.stmt.params.size() ? error_code()
: client_errc::wrong_num_params;
}
constant_string_view query;
span<const format_arg> args;
format_options opts;
inline resultset_encoding get_encoding(const any_execution_request& req)
{
return req.is_query ? resultset_encoding::text : resultset_encoding::binary;
}
void serialize(serialization_context& ctx) const
{
// Create a format context
auto fmt_ctx = access::construct<format_context_base>(output_string_ref::create(ctx), opts);
// Serialize the query header
ctx.add(0x03);
// Serialize the actual query
vformat_sql_to(fmt_ctx, query, args);
// Check for errors
ctx.add_error(fmt_ctx.error_state());
}
};
class start_execution_algo
{
@ -48,15 +67,49 @@ class start_execution_algo
execution_processor& processor() { return read_head_st_.processor(); }
diagnostics& diag() { return read_head_st_.diag(); }
static resultset_encoding get_encoding(any_execution_request::type_t type)
{
switch (type)
{
case any_execution_request::type_t::query:
case any_execution_request::type_t::query_with_params: return resultset_encoding::text;
case any_execution_request::type_t::stmt: return resultset_encoding::binary;
default: BOOST_ASSERT(false); return resultset_encoding::text;
}
}
next_action write_query_with_params(
connection_state_data& st,
any_execution_request::data_t::query_with_params_t data
)
{
// Determine format options
if (st.current_charset.name == nullptr)
{
return error_code(client_errc::unknown_character_set);
}
format_options opts{st.current_charset, st.backslash_escapes};
// Write the request
return st.write(query_with_params{data.query, data.args, opts}, seqnum());
}
next_action write_stmt(connection_state_data& st, any_execution_request::data_t::stmt_t data)
{
if (data.num_params != data.params.size())
return error_code(client_errc::wrong_num_params);
return st.write(execute_stmt_command{data.stmt_id, data.params}, seqnum());
}
next_action compose_request(connection_state_data& st)
{
if (req_.is_query)
switch (req_.type)
{
return st.write(query_command{req_.data.query}, seqnum());
}
else
{
return st.write(execute_stmt_command{req_.data.stmt.stmt.id(), req_.data.stmt.params}, seqnum());
case any_execution_request::type_t::query: return st.write(query_command{req_.data.query}, seqnum());
case any_execution_request::type_t::query_with_params:
return write_query_with_params(st, req_.data.query_with_params);
case any_execution_request::type_t::stmt: return write_stmt(st, req_.data.stmt);
default: BOOST_ASSERT(false); return next_action();
}
}
@ -77,17 +130,11 @@ public:
// Clear diagnostics
diag().clear();
// Check for errors
ec = check_client_errors(req_);
if (ec)
return ec;
// Reset the processor
processor().reset(get_encoding(req_), st.meta_mode);
processor().reset(get_encoding(req_.type), st.meta_mode);
// Send the execution request
BOOST_MYSQL_YIELD(resume_point_, 1, compose_request(st))
if (ec)
return ec;
@ -105,4 +152,4 @@ public:
} // namespace mysql
} // namespace boost
#endif /* INCLUDE_MYSQL_IMPL_NETWORK_ALGORITHMS_READ_RESULTSET_HEAD_HPP_ */
#endif

View File

@ -10,11 +10,18 @@
#pragma once
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/statement.hpp>
#include <boost/mysql/detail/access.hpp>
#include <boost/mysql/detail/any_execution_request.hpp>
#include <boost/mysql/detail/writable_field_traits.hpp>
#include <boost/assert.hpp>
#include <boost/core/ignore_unused.hpp>
#include <tuple>
#include <vector>
template <BOOST_MYSQL_WRITABLE_FIELD_TUPLE WritableFieldTuple>
class boost::mysql::bound_statement_tuple
@ -79,4 +86,60 @@ boost::mysql::bound_statement_iterator_range<FieldViewFwdIterator> boost::mysql:
return bound_statement_iterator_range<FieldViewFwdIterator>(*this, first, last);
}
// Execution request traits
namespace boost {
namespace mysql {
namespace detail {
// Tuple
template <std::size_t N>
struct stmt_tuple_request_proxy
{
statement stmt;
std::array<field_view, N> params;
operator any_execution_request() const
{
return any_execution_request({stmt.id(), static_cast<std::uint16_t>(stmt.num_params()), params});
}
};
template <class... T>
struct execution_request_traits<bound_statement_tuple<std::tuple<T...>>>
{
template <std::size_t... I>
static std::array<field_view, sizeof...(T)> tuple_to_array(const std::tuple<T...>& t, mp11::index_sequence<I...>)
{
boost::ignore_unused(t); // MSVC gets confused if sizeof...(T) == 0
return {{to_field(std::get<I>(t))...}};
}
static stmt_tuple_request_proxy<sizeof...(T)> make_request(const bound_statement_tuple<std::tuple<T...>>& input, std::vector<field_view>&)
{
auto& impl = access::get_impl(input);
return {impl.stmt, tuple_to_array(impl.params, mp11::make_index_sequence<sizeof...(T)>())};
}
};
// Iterator range
template <class FieldViewFwdIterator>
struct execution_request_traits<bound_statement_iterator_range<FieldViewFwdIterator>>
{
static any_execution_request make_request(
const bound_statement_iterator_range<FieldViewFwdIterator>& input,
std::vector<field_view>& shared_fields
)
{
auto& impl = access::get_impl(input);
shared_fields.assign(impl.first, impl.last);
return any_execution_request(
{impl.stmt.id(), static_cast<std::uint16_t>(impl.stmt.num_params()), shared_fields}
);
}
};
} // namespace detail
} // namespace mysql
} // namespace boost
#endif

View File

@ -0,0 +1,85 @@
//
// 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_IMPL_WITH_PARAMS_HPP
#define BOOST_MYSQL_IMPL_WITH_PARAMS_HPP
#pragma once
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/mysql/detail/any_execution_request.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/core/span.hpp>
#include <boost/mp11/integer_sequence.hpp>
#include <utility>
// Execution request traits
namespace boost {
namespace mysql {
namespace detail {
template <std::size_t N>
struct with_params_proxy
{
constant_string_view query;
std::array<format_arg, N> args;
operator detail::any_execution_request() const { return any_execution_request({query, args}); }
};
template <class... T>
struct execution_request_traits<with_params_t<T...>>
{
template <class WithParamsType, std::size_t... I>
static with_params_proxy<sizeof...(T)> make_request_impl(WithParamsType&& input, mp11::index_sequence<I...>)
{
boost::ignore_unused(input); // MSVC gets confused for tuples of size 0
// clang-format off
return {
input.query,
{{
{
string_view(),
formattable_ref(std::get<I>(std::forward<WithParamsType>(input).args))
}...
}}
};
// clang-format on
}
// Allow the value category of the object to be deduced
template <class WithParamsType>
static with_params_proxy<sizeof...(T)> make_request(WithParamsType&& input, std::vector<field_view>&)
{
return make_request_impl(
std::forward<WithParamsType>(input),
mp11::make_index_sequence<sizeof...(T)>()
);
}
};
// Old MSVCs fail to process the above when sizeof...(T) is zero
template <>
struct execution_request_traits<with_params_t<>>
{
static any_execution_request make_request(with_params_t<> input, std::vector<field_view>&)
{
return any_execution_request({input.query, span<format_arg>{}});
}
};
} // namespace detail
} // namespace mysql
} // namespace boost
#endif

View File

@ -0,0 +1,120 @@
//
// 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_WITH_PARAMS_HPP
#define BOOST_MYSQL_WITH_PARAMS_HPP
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/detail/format_sql.hpp>
#include <tuple>
#include <utility>
namespace boost {
namespace mysql {
/**
* \brief Type trait that applies the transformation performed by `std::make_tuple` to a single element.
* \details
* For example: \n
* - `make_tuple_element_t<int>` yields `int` \n
* - `make_tuple_element_t<const int&>` yields `int` \n
* - `make_tuple_element_t<std::reference_wrapper<int>>` yields `int&` \n
* \n
* Consult the <a href="https://en.cppreference.com/w/cpp/utility/tuple/make_tuple">`std::make_tuple`</a> docs
* for more info.
*/
template <class T>
using make_tuple_element_t = typename std::tuple_element<0, decltype(std::make_tuple(std::declval<T&&>()))>::
type;
/**
* \brief A query format string and format arguments that can be executed.
* \details
* Contains a query with placeholders (i.e. `{}`) and a set of parameters to
* expand such placeholders. Satisfies `ExecutionRequest` and can thus be passed
* to \ref any_connection::execute, \ref any_connection::start_execution and its
* async counterparts.
* \n
* When executed, client-side SQL formatting is invoked
* to expand the provided query with the supplied parameters. The resulting query is then sent to
* the server for execution. Formally, given a `conn` variable of \ref any_connection type,
* the query is generated as if the following was called:
* ```
* format_sql(
* this->query,
* conn.format_opts().value(),
* std::get<i>(this->args)... // for i in [0, sizeof...(Formattable))
* );
* ```
* \n
* Objects of this type are usually created using \ref with_params, which
* creates `args` by calling `std::make_tuple`.
*
* \par Object lifetimes
* The format string `query` is stored as a view, as a compile-time string should be used in most cases.
* When using \ref with_params, `args` will usually contain copies of the passed parameters
* (as per <a href="https://en.cppreference.com/w/cpp/utility/tuple/make_tuple">`std::make_tuple`</a>),
* which is safe even when using async functions with deferred completion tokens.
* You may disable such copies using `std::ref`, as you would when using `std::make_tuple`.
*
* \par Errors
* When passed to \ref any_connection::execute, \ref any_connection::start_execution or
* its async counterparts, in addition to the usual network and server-generated errors,
* `with_params_t` may generate the following errors: \n
* - Any errors generated by \ref format_sql. This includes errors due to invalid format
* strings and unformattable arguments (e.g. invalid UTF-8).
* - \ref client_errc::unknown_character_set if the connection does not know the
* character set it's using when the query is executed (i.e. \ref any_connection::current_character_set
* would return an error.
*/
template <BOOST_MYSQL_FORMATTABLE... Formattable>
struct with_params_t
{
/// The query to be expanded and executed, which may contain `{}` placeholders.
constant_string_view query;
/// The arguments to use to expand the query.
std::tuple<Formattable...> args;
};
/**
* \brief Creates a query with parameters (client-side SQL formatting) that can be executed.
* \details
* Creates a \ref with_params_t object by packing the supplied arguments into a tuple,
* calling <a href="https://en.cppreference.com/w/cpp/utility/tuple/make_tuple">`std::make_tuple`</a>.
* As per `std::make_tuple`, parameters will be decay-copied into the resulting object.
* This behavior can be disabled by passing `std::reference_wrapper` objects, which are
* transformed into references.
* \n
* This function does not inspect the supplied query string and arguments.
* Errors like missing format arguments are detected when the resulting object is executed.
* This function does not involve communication with the server.
* \n
* The passed `args` must either satisfy `Formattable`, or be `std::reference_wrapper<T>`
* with `T` satisfying `Formattable`.
* \n
* See \ref with_params_t for details on how the execution request works.
* \n
* \par Exception safety
* Strong guarantee. Any exception thrown when copying `args` will be propagated.
* \n
*/
template <class... FormattableOrRefWrapper>
auto with_params(constant_string_view query, FormattableOrRefWrapper&&... args)
-> with_params_t<make_tuple_element_t<FormattableOrRefWrapper>...>
{
return {query, std::make_tuple(std::forward<FormattableOrRefWrapper>(args)...)};
}
} // namespace mysql
} // namespace boost
#include <boost/mysql/impl/with_params.hpp>
#endif

View File

@ -7,20 +7,32 @@
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/execution_state.hpp>
#include <boost/mysql/field.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/optional/optional.hpp>
#include <boost/test/unit_test.hpp>
#include <forward_list>
#include <functional>
#include <vector>
#include "test_common/create_basic.hpp"
#include "test_common/has_ranges.hpp"
#include "test_common/network_result.hpp"
#include "test_integration/any_connection_fixture.hpp"
#ifdef BOOST_MYSQL_HAS_RANGES
#include <ranges>
#endif
using namespace boost::mysql;
using namespace boost::mysql::test;
using boost::test_tools::per_element;
namespace asio = boost::asio;
// Cover all possible execution requests for execute() and async_execute()
@ -39,7 +51,11 @@ BOOST_FIXTURE_TEST_CASE(query, any_connection_fixture)
conn.async_execute("SELECT 1", r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(1, 1), per_element());
// start execution
// types convertible to string_view work
conn.async_execute(std::string("SELECT 1"), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(1, 1), per_element());
// spotcheck: start execution with a text query works
conn.async_start_execution("SELECT 1", st, as_netresult).validate_no_error();
auto rws = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST(rws == makerows(1, 1), per_element());
@ -47,37 +63,38 @@ BOOST_FIXTURE_TEST_CASE(query, any_connection_fixture)
BOOST_FIXTURE_TEST_CASE(stmt_tuple, any_connection_fixture)
{
// Also verifies that tuples correctly apply the writable field transformation
// Setup
connect();
results r;
execution_state st;
auto stmt = conn.async_prepare_statement("SELECT ?", as_netresult).get();
BOOST_TEST_REQUIRE(stmt.num_params() == 1u);
auto stmt = conn.async_prepare_statement("SELECT ?, ?", as_netresult).get();
BOOST_TEST_REQUIRE(stmt.num_params() == 2u);
// execute
conn.async_execute(stmt.bind("42"), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(1, "42"), per_element());
conn.async_execute(stmt.bind("42", boost::optional<int>(13)), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, "42", 13), per_element());
// start execution
conn.async_start_execution(stmt.bind(90), st, as_netresult).validate_no_error();
// references work
std::string s = "abcdef";
auto op = conn.async_execute(stmt.bind(std::ref(s), 21), r, asio::deferred);
s = "opqrs";
std::move(op)(as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, "opqrs", 21), per_element());
// spotcheck: start execution with tuples works
conn.async_start_execution(stmt.bind("abc", boost::optional<int>()), st, as_netresult)
.validate_no_error();
auto rws = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST(rws == makerows(1, 90), per_element());
}
BOOST_TEST(rws == makerows(2, "abc", nullptr), per_element());
BOOST_FIXTURE_TEST_CASE(stmt_tuple_error, any_connection_fixture)
{
// Setup
connect();
results r;
execution_state st;
auto stmt = conn.async_prepare_statement("SELECT ?", as_netresult).get();
BOOST_TEST_REQUIRE(stmt.num_params() == 1u);
// spotcheck: errors correctly detected
conn.async_execute(stmt.bind("42"), r, as_netresult).validate_error(client_errc::wrong_num_params);
// execute
conn.async_execute(stmt.bind("42", 200), r, as_netresult).validate_error(client_errc::wrong_num_params);
// start execution
conn.async_start_execution(stmt.bind(), st, as_netresult).validate_error(client_errc::wrong_num_params);
// spotcheck: lvalues work
const auto bound_stmt = stmt.bind("42", boost::optional<int>());
conn.async_execute(bound_stmt, r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, "42", nullptr), per_element());
}
BOOST_FIXTURE_TEST_CASE(stmt_range, any_connection_fixture)
@ -94,29 +111,77 @@ BOOST_FIXTURE_TEST_CASE(stmt_range, any_connection_fixture)
conn.async_execute(stmt.bind(params.begin(), params.end()), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, 42, "abc"), per_element());
// start execution
// spotcheck: statement with ranges work with start execution
conn.async_start_execution(stmt.bind(params.begin(), params.end()), st, as_netresult).validate_no_error();
auto rws = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST(rws == makerows(2, 42, "abc"), per_element());
// Regression check: executing with a type convertible (but not equal) to field_view works
const std::vector<field> owning_params{field_view(50), field_view("luv")};
conn.async_execute(stmt.bind(owning_params.begin(), owning_params.end()), r, as_netresult)
.validate_no_error();
BOOST_TEST(r.rows() == makerows(2, 50, "luv"), per_element());
// Spotcheck: errors detected
auto too_few_params = make_fv_arr(1);
conn.async_execute(stmt.bind(too_few_params.begin(), too_few_params.end()), r, as_netresult)
.validate_error(client_errc::wrong_num_params);
// Spotchecks: lvalues work
auto bound_stmt = stmt.bind(params.begin(), params.end());
conn.async_execute(bound_stmt, r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, 42, "abc"), per_element());
}
BOOST_FIXTURE_TEST_CASE(stmt_range_error, any_connection_fixture)
BOOST_FIXTURE_TEST_CASE(with_params_, any_connection_fixture)
{
// Setup
connect();
results r;
execution_state st;
std::forward_list<field_view> params{field_view(42)};
auto stmt = conn.async_prepare_statement("SELECT ?, ?", as_netresult).get();
BOOST_TEST_REQUIRE(stmt.num_params() == 2u);
// execute
conn.async_execute(stmt.bind(params.begin(), params.end()), r, as_netresult)
.validate_error(client_errc::wrong_num_params);
conn.async_execute(with_params("SELECT {}, {}", 42, "abc"), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, 42, "abc"), per_element());
// start execution
conn.async_start_execution(stmt.bind(params.begin(), params.end()), st, as_netresult)
.validate_error(client_errc::wrong_num_params);
// spotcheck: can be used with start_execution
conn.async_start_execution(with_params("SELECT {}, {}", 42, "abc"), st, as_netresult).validate_no_error();
auto rws = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST(rws == makerows(2, 42, "abc"), per_element());
// references work
std::string s = "abcdef";
auto op = conn.async_execute(with_params("SELECT {}, {}", 42, std::ref(s)), r, asio::deferred);
s = "opqrs";
std::move(op)(as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, 42, "opqrs"), per_element());
// Queries without parameters work
conn.async_execute(with_params("SELECT '{{}}'"), r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(1, "{}"), per_element());
// lvalues work
const auto req = with_params("SELECT {}, {}", "42", boost::optional<int>(100));
conn.async_execute(req, r, as_netresult).validate_no_error();
BOOST_TEST(r.rows() == makerows(2, "42", 100), per_element());
#ifdef BOOST_MYSQL_HAS_RANGES
// Regression check: mutable ranges work
std::vector<int> nums{2, 5, 10, 20};
auto is_even = [](int i) { return i % 2 == 0; };
conn.async_execute(with_params("SELECT {}", std::ranges::views::filter(nums, is_even)), r, as_netresult)
.validate_no_error();
BOOST_TEST(r.rows() == makerows(3, 2, 10, 20), per_element());
#endif
// Error: unformattable values
conn.async_execute(with_params("SELECT {}", "bad\xffutf8"), r, as_netresult)
.validate_error(client_errc::invalid_encoding);
// Error: unknown charset
conn.async_reset_connection(as_netresult).validate_no_error();
conn.async_execute(with_params("SELECT {}", 42), r, as_netresult)
.validate_error(client_errc::unknown_character_set);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -13,6 +13,7 @@
#include <boost/mysql/metadata_mode.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/optional/optional.hpp>
@ -24,6 +25,7 @@
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "test_common/has_ranges.hpp"
#include "test_common/printing.hpp"
@ -70,7 +72,10 @@ std::string compose_select_query(
boost::mysql::format_sql_to(ctx, " LIMIT {}", *limit);
}
// Retrieve the generated query string
// Retrieve the generated query string.
// get() returns a boost::system::result<std::string> that
// contains an error if any of the format operations failed.
// Calling value() will throw on error, like format_sql does
return std::move(ctx).get().value();
}
//]
@ -129,138 +134,98 @@ BOOST_AUTO_TEST_CASE(section_sql_formatting)
{
//[sql_formatting_simple
std::string employee_name = get_name(); // employee_name is an untrusted string
// Compose the SQL query in the client
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id, salary FROM employee WHERE last_name = {}",
employee_name
);
// If employee_name is "John", query now contains:
// "SELECT id, salary FROM employee WHERE last_name = 'John'"
// If employee_name contains quotes, they will be escaped as required
// Execute the generated query as usual
results result;
conn.execute(query, result);
//]
BOOST_TEST(query == "SELECT id, salary FROM employee WHERE last_name = 'John'");
// Expand the query and execute it. The expansion happens client-side.
// If employee_name is "John", the executed query would be:
// "SELECT id, salary FROM employee WHERE last_name = 'John'"
conn.execute(
with_params("SELECT id, salary FROM employee WHERE last_name = {}", employee_name),
result
);
//]
}
{
//[sql_formatting_other_scalars
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id FROM employee WHERE salary > {}",
42000
);
BOOST_TEST(query == "SELECT id FROM employee WHERE salary > 42000");
// Will execute "SELECT id FROM employee WHERE salary > 42000"
results result;
conn.execute(with_params("SELECT id FROM employee WHERE salary > {}", 42000), result);
//]
conn.execute(query, r);
}
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
{
//[sql_formatting_optionals
std::optional<std::int64_t> salary; // get salary from a possibly untrusted source
results result;
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"UPDATE employee SET salary = {} WHERE id = {}",
salary,
1
);
// Depending on whether salary has a value or not, generates:
// UPDATE employee SET salary = 42000 WHERE id = 1
// UPDATE employee SET salary = NULL WHERE id = 1
// Depending on whether salary has a value or not, executes:
// "UPDATE employee SET salary = 42000 WHERE id = 1"
// "UPDATE employee SET salary = NULL WHERE id = 1"
conn.execute(with_params("UPDATE employee SET salary = {} WHERE id = {}", salary, 1), result);
//]
BOOST_TEST(query == "UPDATE employee SET salary = NULL WHERE id = 1");
conn.execute(query, r);
}
#endif
{
//[sql_formatting_ranges
results result;
std::vector<long> ids{1, 5, 20};
std::string query = format_sql(
conn.format_opts().value(),
"SELECT * FROM employee WHERE id IN ({})",
ids
);
BOOST_TEST(query == "SELECT * FROM employee WHERE id IN (1, 5, 20)");
// Executes "SELECT * FROM employee WHERE id IN (1, 5, 20)"
conn.execute(with_params("SELECT * FROM employee WHERE id IN ({})", ids), result);
//]
conn.execute(query, r);
}
{
//[sql_formatting_manual_indices
// Recall that you need to set connect_params::multi_queries to true when connecting
// before running semicolon-separated queries.
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"UPDATE employee SET first_name = {1} WHERE id = {0}; SELECT * FROM employee WHERE id = {0}",
42,
"John"
);
BOOST_TEST(
query ==
"UPDATE employee SET first_name = 'John' WHERE id = 42; SELECT * FROM employee WHERE id = 42"
// before running semicolon-separated queries. Executes:
// "UPDATE employee SET first_name = 'John' WHERE id = 42; SELECT * FROM employee WHERE id = 42"
results result;
conn.execute(
with_params(
"UPDATE employee SET first_name = {1} WHERE id = {0}; SELECT * FROM employee WHERE id = {0}",
42,
"John"
),
result
);
//]
conn.execute(query, r);
}
{
// clang-format off
//[sql_formatting_named_args
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"UPDATE employee SET first_name = {name} WHERE id = {id}; SELECT * FROM employee WHERE id = {id}",
{
{"id", 42 },
{"name", "John"}
}
);
//<-
// clang-format on
//->
BOOST_TEST(
query ==
"UPDATE employee SET first_name = 'John' WHERE id = 42; SELECT * FROM employee WHERE id = 42"
);
//[sql_formatting_invalid_encoding
try
{
// If the connection is using UTF-8 (the default), this will throw an error,
// because the string to be formatted is not valid UTF-8.
// The query never reaches the server.
results result;
conn.execute(with_params("SELECT {}", "bad\xff UTF-8"), result);
//<-
BOOST_TEST(false);
//->
}
catch (const boost::system::system_error& err)
{
BOOST_TEST(err.code() == boost::mysql::client_errc::invalid_encoding);
}
//]
conn.execute(query, r);
}
{
//[sql_formatting_specifiers
//[sql_formatting_format_sql
// Compose the SQL query without executing it.
// format_opts returns a system::result<format_options>,
// contains settings like the current character set.
// If the connection is using an unknown character set, this will throw an error.
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id, last_name FROM employee ORDER BY {:i} DESC",
"company_id"
"SELECT id, salary FROM employee WHERE last_name = {}",
"Doe"
);
BOOST_TEST(query == "SELECT id, last_name FROM employee ORDER BY `company_id` DESC");
BOOST_TEST(query == "SELECT id, salary FROM employee WHERE last_name = 'Doe'");
//]
conn.execute(query, r);
}
{
//[sql_formatting_specifiers_explicit_indices
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id, last_name FROM employee ORDER BY {0:i} DESC",
"company_id"
);
//]
BOOST_TEST(query == "SELECT id, last_name FROM employee ORDER BY `company_id` DESC");
conn.execute(query, r);
}
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
{
//[sql_formatting_incremental_use
@ -330,22 +295,45 @@ BOOST_AUTO_TEST_CASE(section_sql_formatting)
//]
conn.execute(query, r);
}
{
try
{
//[sql_formatting_invalid_encoding
// If the connection is using UTF-8 (the default), this will throw an error,
// because the string to be formatted contains invalid UTF8.
format_sql(conn.format_opts().value(), "SELECT {}", "bad\xff UTF-8");
//]
BOOST_TEST(false);
}
catch (const boost::system::system_error& err)
{
BOOST_TEST(err.code() == boost::mysql::client_errc::invalid_encoding);
}
{
//[sql_formatting_specifiers
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id, last_name FROM employee ORDER BY {:i} DESC",
"company_id"
);
BOOST_TEST(query == "SELECT id, last_name FROM employee ORDER BY `company_id` DESC");
//]
conn.execute(query, r);
}
{
//[sql_formatting_specifiers_explicit_indices
std::string query = boost::mysql::format_sql(
conn.format_opts().value(),
"SELECT id, last_name FROM employee ORDER BY {0:i} DESC",
"company_id"
);
//]
BOOST_TEST(query == "SELECT id, last_name FROM employee ORDER BY `company_id` DESC");
conn.execute(query, r);
}
{
//[sql_formatting_empty_ranges
// If ids.empty(), generates "SELECT * FROM employee WHERE id IN ()", which is a syntax error.
// This is not a security issue for this query, but may be exploitable in more involved scenarios.
// Queries involving only scalar values (as opposed to ranges) are not affected by this.
// It is your responsibility to check for conditions like ids.empty(), as client-side SQL
// formatting does not understand your queries.
std::vector<int> ids;
auto q = format_sql(conn.format_opts().value(), "SELECT * FROM employee WHERE id IN ({})", ids);
//]
BOOST_TEST(q == "SELECT * FROM employee WHERE id IN ()");
}
{
using namespace boost::mysql;
using boost::optional;

View File

@ -79,6 +79,7 @@ add_executable(
test/spotchecks/default_completion_tokens.cpp
test/spotchecks/read_some_rows_static.cpp
test/spotchecks/multifn.cpp
test/spotchecks/execution_requests.cpp
test/spotchecks/misc.cpp
test/format_sql/formattable.cpp

View File

@ -88,6 +88,7 @@ run
test/spotchecks/default_completion_tokens.cpp
test/spotchecks/read_some_rows_static.cpp
test/spotchecks/multifn.cpp
test/spotchecks/execution_requests.cpp
test/spotchecks/misc.cpp
test/format_sql/formattable.cpp

View File

@ -0,0 +1,26 @@
//
// 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_UNIT_INCLUDE_TEST_UNIT_TEST_ANY_CONNECTION_HPP
#define BOOST_MYSQL_TEST_UNIT_INCLUDE_TEST_UNIT_TEST_ANY_CONNECTION_HPP
#include <boost/mysql/any_connection.hpp>
#include "test_unit/test_stream.hpp"
namespace boost {
namespace mysql {
namespace test {
any_connection create_test_any_connection(any_connection_params params = {});
test_stream& get_stream(any_connection& conn);
} // namespace test
} // namespace mysql
} // namespace boost
#endif

View File

@ -5,8 +5,14 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/detail/access.hpp>
#include <boost/mysql/detail/engine.hpp>
#include <boost/mysql/detail/engine_impl.hpp>
#include <boost/mysql/detail/engine_stream_adaptor.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/coroutine.hpp>
@ -18,11 +24,13 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <set>
#include <vector>
#include "test_common/buffer_concat.hpp"
#include "test_common/tracker_executor.hpp"
#include "test_unit/test_any_connection.hpp"
#include "test_unit/test_stream.hpp"
using namespace boost::mysql::test;
@ -186,3 +194,18 @@ test_stream& boost::mysql::test::test_stream::add_break(std::size_t byte_num)
read_break_offsets_.insert(byte_num);
return *this;
}
// test_any_connection
boost::mysql::any_connection boost::mysql::test::create_test_any_connection(any_connection_params params)
{
return any_connection(detail::access::construct<any_connection>(
std::unique_ptr<detail::engine>(new detail::engine_impl<detail::engine_stream_adaptor<test_stream>>()
),
params
));
}
test_stream& boost::mysql::test::get_stream(any_connection& conn)
{
return detail::stream_from_engine<test_stream>(detail::access::get_impl(conn).get_engine());
}

View File

@ -5,6 +5,8 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/mysql/with_params.hpp>
#include <boost/mysql/detail/config.hpp>
#ifdef BOOST_MYSQL_HAS_CONCEPTS
@ -69,6 +71,17 @@ static_assert(is_execution_request<bound_statement_iterator_range<field_view*>&&
static_assert(!is_execution_request<field_view*>::value, "");
// with_params
static_assert(is_execution_request<with_params_t<>>::value, "");
static_assert(is_execution_request<with_params_t<int>>::value, "");
static_assert(is_execution_request<with_params_t<int, float>>::value, "");
static_assert(is_execution_request<with_params_t<const std::string&, float>>::value, "");
static_assert(is_execution_request<with_params_t<int>&>::value, "");
static_assert(is_execution_request<const with_params_t<int>&>::value, "");
static_assert(is_execution_request<with_params_t<int>&&>::value, "");
static_assert(is_execution_request<with_params_t<const std::string&>&&>::value, "");
// Other stuff
static_assert(!is_execution_request<field_view>::value, "");
static_assert(!is_execution_request<int>::value, "");

View File

@ -439,6 +439,38 @@ BOOST_AUTO_TEST_CASE(success_after_error)
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
}
BOOST_AUTO_TEST_CASE(add_error)
{
// Setup
std::vector<std::uint8_t> buff;
detail::serialization_context ctx(buff);
// Successfully add some data
ctx.add(std::vector<std::uint8_t>{1, 2, 3, 4, 5});
std::vector<std::uint8_t> expected{0, 0, 0, 0, 1, 2, 3, 4, 5};
BOOST_TEST(ctx.error() == error_code());
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
// Add an error
ctx.add_error(client_errc::invalid_encoding);
BOOST_TEST(ctx.error() == client_errc::invalid_encoding);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
// Adding further data with the error set does nothing
ctx.add(std::vector<std::uint8_t>{8, 9, 10});
BOOST_TEST(ctx.error() == client_errc::invalid_encoding);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
// Adding another error does nothing
ctx.add_error(client_errc::protocol_value_error);
BOOST_TEST(ctx.error() == client_errc::invalid_encoding);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
ctx.add_error(error_code());
BOOST_TEST(ctx.error() == client_errc::invalid_encoding);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(buff, expected);
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE_END()

View File

@ -25,7 +25,6 @@
#include "test_unit/create_ok.hpp"
#include "test_unit/create_ok_frame.hpp"
#include "test_unit/create_row_message.hpp"
#include "test_unit/create_statement.hpp"
#include "test_unit/mock_execution_processor.hpp"
#include "test_unit/printing.hpp"
@ -239,15 +238,11 @@ BOOST_AUTO_TEST_CASE(execute_success_rows)
BOOST_AUTO_TEST_CASE(execute_error_num_params)
{
// Setup
auto stmt = statement_builder().id(1).num_params(2).build();
const auto params = make_fv_arr("test", nullptr, 42); // too many params
execute_fixture fix(any_execution_request(stmt, params));
execute_fixture fix(any_execution_request({std::uint32_t(1), std::uint16_t(2), params}));
// Run the algo. Nothing should be written to the server
algo_test().check(fix, client_errc::wrong_num_params);
// We didn't modify the processor
fix.proc.num_calls().validate();
}
// Tests error on write, while reading head and while reading rows (error spotcheck)

View File

@ -5,6 +5,7 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/column_type.hpp>
#include <boost/mysql/diagnostics.hpp>
@ -19,6 +20,7 @@
#include <boost/test/unit_test.hpp>
#include <array>
#include <string>
#include "test_common/check_meta.hpp"
@ -27,7 +29,9 @@
#include "test_unit/create_coldef_frame.hpp"
#include "test_unit/create_frame.hpp"
#include "test_unit/create_meta.hpp"
#include "test_unit/create_statement.hpp"
#include "test_unit/create_ok.hpp"
#include "test_unit/create_ok_frame.hpp"
#include "test_unit/create_query_frame.hpp"
#include "test_unit/mock_execution_processor.hpp"
#include "test_unit/printing.hpp"
@ -72,12 +76,11 @@ BOOST_AUTO_TEST_CASE(text_query)
fix.proc.num_calls().reset(1).on_num_meta(1).on_meta(1).validate();
}
BOOST_AUTO_TEST_CASE(prepared_statement)
BOOST_AUTO_TEST_CASE(stmt_success)
{
// Setup
auto stmt = statement_builder().id(1).num_params(2).build();
const auto params = make_fv_arr("test", nullptr);
fixture fix(any_execution_request(stmt, params));
fixture fix(any_execution_request({std::uint32_t(1u), std::uint16_t(2u), params}));
// Run the algo
algo_test()
@ -100,18 +103,60 @@ BOOST_AUTO_TEST_CASE(prepared_statement)
fix.proc.num_calls().reset(1).on_num_meta(1).on_meta(1).validate();
}
BOOST_AUTO_TEST_CASE(error_num_params)
BOOST_AUTO_TEST_CASE(stmt_error_num_params)
{
// Setup
auto stmt = statement_builder().id(1).num_params(2).build();
const auto params = make_fv_arr("test", nullptr, 42); // too many params
fixture fix(any_execution_request(stmt, params));
fixture fix(any_execution_request({std::uint32_t(1u), std::uint16_t(2u), params}));
// Run the algo. Nothing should be written to the server
algo_test().check(fix, client_errc::wrong_num_params);
}
// We didn't modify the processor
fix.proc.num_calls().validate();
BOOST_AUTO_TEST_CASE(with_params_success)
{
// Setup
const std::array<format_arg, 2> args{
{{"", "abc"}, {"", 42}}
};
fixture fix(any_execution_request({"SELECT {}, {}", args}));
fix.st.current_charset = utf8mb4_charset;
// Run the algo
algo_test()
.expect_write(create_query_frame(0, "SELECT 'abc', 42"))
.expect_read(create_ok_frame(1, ok_builder().build()))
.check(fix);
// Verify
BOOST_TEST(fix.proc.encoding() == resultset_encoding::text);
BOOST_TEST(fix.proc.sequence_number() == 2u);
BOOST_TEST(fix.proc.is_complete());
fix.proc.num_calls().reset(1).on_head_ok_packet(1).validate();
}
BOOST_AUTO_TEST_CASE(with_params_error_unknown_charset)
{
// Setup
const std::array<format_arg, 2> args{
{{"", "abc"}, {"", 42}}
};
fixture fix(any_execution_request({"SELECT {}, {}", args}));
fix.st.current_charset = {};
// The algo fails immediately
algo_test().check(fix, client_errc::unknown_character_set);
}
BOOST_AUTO_TEST_CASE(with_params_error_formatting)
{
// Setup
const std::array<format_arg, 1> args{{{"", "abc"}}};
fixture fix(any_execution_request({"SELECT {}, {}", args}));
fix.st.current_charset = utf8mb4_charset;
// The algo fails immediately
algo_test().check(fix, client_errc::format_arg_not_found);
}
// This covers errors in both writing the request and calling read_resultset_head

View File

@ -5,86 +5,59 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/mysql/connection.hpp>
#include <boost/mysql/results.hpp>
#include <boost/test/unit_test.hpp>
#include "test_common/netfun_maker.hpp"
#include "test_common/printing.hpp"
#include "test_common/network_result.hpp"
#include "test_unit/create_ok.hpp"
#include "test_unit/create_ok_frame.hpp"
#include "test_unit/test_stream.hpp"
#include "test_unit/test_any_connection.hpp"
using namespace boost::mysql;
using namespace boost::mysql::test;
BOOST_AUTO_TEST_SUITE(test_connection_use_after_move)
using test_connection = connection<test_stream>;
using query_netfun_maker = netfun_maker<void, test_connection, const string_view&, results&>;
struct
{
query_netfun_maker::signature query;
const char* name;
} all_fns[] = {
{query_netfun_maker::sync_errc(&test_connection::execute), "sync" },
{query_netfun_maker::async_diag(&test_connection::async_execute), "async"},
};
BOOST_AUTO_TEST_CASE(use_move_constructed_connection)
{
for (const auto& fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
// Construct connection
test_connection conn;
// Construct connection
auto conn = create_test_any_connection();
// Use it
conn.stream().add_bytes(create_ok_frame(1, ok_builder().build()));
results result;
fns.query(conn, "SELECT * FROM myt", result).validate_no_error();
// Use it
get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build()));
results result;
conn.async_execute("SELECT * FROM myt", result, as_netresult).validate_no_error();
// Move construct another connection from conn
test_connection conn2(std::move(conn));
// Move construct another connection from conn
any_connection conn2(std::move(conn));
// Using it works (no dangling pointer)
conn2.stream().add_bytes(create_ok_frame(1, ok_builder().affected_rows(42).build()));
fns.query(conn2, "DELETE FROM myt", result).validate_no_error();
BOOST_TEST(result.affected_rows() == 42u);
}
}
// Using it works (no dangling pointer)
get_stream(conn2).add_bytes(create_ok_frame(1, ok_builder().affected_rows(42).build()));
conn2.async_execute("DELETE FROM myt", result, as_netresult).validate_no_error();
BOOST_TEST(result.affected_rows() == 42u);
}
BOOST_AUTO_TEST_CASE(use_move_assigned_connection)
{
for (const auto& fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
// Construct two connections
test_connection conn1;
test_connection conn2;
// Construct two connections
auto conn1 = create_test_any_connection();
auto conn2 = create_test_any_connection();
// Use them
conn1.stream().add_bytes(create_ok_frame(1, ok_builder().build()));
conn2.stream().add_bytes(create_ok_frame(1, ok_builder().build()));
results result;
fns.query(conn1, "SELECT * FROM myt", result).validate_no_error();
fns.query(conn2, "SELECT * FROM myt", result).validate_no_error();
// Use them
get_stream(conn1).add_bytes(create_ok_frame(1, ok_builder().build()));
get_stream(conn2).add_bytes(create_ok_frame(1, ok_builder().build()));
results result;
conn1.async_execute("SELECT * FROM myt", result, as_netresult).validate_no_error();
conn2.async_execute("SELECT * FROM myt", result, as_netresult).validate_no_error();
// Move assign
conn2 = std::move(conn1);
// Move assign
conn2 = std::move(conn1);
// Using it works (no dangling pointer)
conn2.stream().add_bytes(create_ok_frame(1, ok_builder().affected_rows(42).build()));
fns.query(conn2, "DELETE FROM myt", result).validate_no_error();
BOOST_TEST(result.affected_rows() == 42u);
}
}
// Using it works (no dangling pointer)
get_stream(conn2).add_bytes(create_ok_frame(1, ok_builder().affected_rows(42).build()));
conn2.async_execute("DELETE FROM myt", result, as_netresult).validate_no_error();
BOOST_TEST(result.affected_rows() == 42u);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -0,0 +1,628 @@
//
// 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)
//
// Since integration tests can't reliably test multifunction operations
// that span over multiple messages, we test the complete multifn fllow in this unit tests.
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/execution_state.hpp>
#include <boost/mysql/results.hpp>
#include <boost/mysql/rows_view.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/tcp.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/mysql/detail/access.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/test/unit_test.hpp>
#include <boost/test/unit_test_suite.hpp>
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "test_common/assert_buffer_equals.hpp"
#include "test_common/network_result.hpp"
#include "test_common/tracker_executor.hpp"
#include "test_unit/create_ok.hpp"
#include "test_unit/create_ok_frame.hpp"
#include "test_unit/create_query_frame.hpp"
#include "test_unit/create_statement.hpp"
#include "test_unit/test_any_connection.hpp"
#include "test_unit/test_stream.hpp"
using namespace boost::mysql;
using namespace boost::mysql::test;
namespace asio = boost::asio;
BOOST_AUTO_TEST_SUITE(test_execution_requests)
// The execution request is forwarded correctly, and non-const objects work correctly.
// This is relevant for with_params involving ranges::views::filter, for instance
// Note: netmakers intentionally not used here
BOOST_AUTO_TEST_SUITE(forwarding_constness)
struct fixture
{
any_connection conn{create_test_any_connection()};
results result;
execution_state st;
fixture() { get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build())); }
void validate_bytes_written()
{
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(
get_stream(conn).bytes_written(),
create_query_frame(0, "SELECT 'abc'")
);
}
struct request
{
string_view value;
operator string_view() { return value; } // Intentionally non-const
};
static request make_request() { return {"SELECT 'abc'"}; }
};
BOOST_FIXTURE_TEST_CASE(execute_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Run
conn.execute(make_request(), result, ec, diag);
// Validate
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_sync_exc, fixture)
{
conn.execute(make_request(), result);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_diag, fixture)
{
diagnostics diag;
conn.async_execute(make_request(), result, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_nodiag, fixture)
{
conn.async_execute(make_request(), result, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_deferred, fixture)
{
// Spotcheck: deferred works correctly
auto op = conn.async_execute(make_request(), result, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Run
conn.start_execution(make_request(), st, ec, diag);
// Validate
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_exc, fixture)
{
conn.start_execution(make_request(), st);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_diag, fixture)
{
diagnostics diag;
conn.async_start_execution(make_request(), st, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_nodiag, fixture)
{
conn.async_start_execution(make_request(), st, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_deferred, fixture)
{
// Spotcheck: deferred works correctly
auto op = conn.async_start_execution(make_request(), st, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
// Spotcheck: old connection doesn't generate compile errors.
// Intentionally not run
void old_connection()
{
tcp_connection conn(global_context_executor());
auto req = fixture::make_request();
results result;
execution_state st;
error_code ec;
diagnostics diag;
conn.execute(req, result, ec, diag);
conn.execute(req, result);
conn.async_execute(req, result, asio::detached);
conn.async_execute(req, result, diag, asio::detached);
conn.async_execute(req, result, diag, asio::deferred)(asio::detached);
conn.start_execution(req, st, ec, diag);
conn.start_execution(req, st);
conn.async_start_execution(req, st, asio::detached);
conn.async_start_execution(req, st, diag, asio::detached);
conn.async_start_execution(req, st, diag, asio::deferred)(asio::detached);
}
BOOST_AUTO_TEST_SUITE_END()
// The execution request is forwarded with the correct value category, and is moved if required.
// Move-only objects work correctly
BOOST_AUTO_TEST_SUITE(forwarding_rvalues)
struct fixture
{
any_connection conn{create_test_any_connection()};
results result;
execution_state st;
fixture() { get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build())); }
void validate_bytes_written()
{
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(
get_stream(conn).bytes_written(),
create_query_frame(0, "SELECT 'abcd'")
);
}
// An execution request that doesn't support copies
struct request
{
std::unique_ptr<char[]> buff;
std::size_t size;
request(string_view v) : buff(new char[v.size()]), size(v.size())
{
std::copy(v.begin(), v.end(), buff.get());
}
operator string_view() const { return {buff.get(), size}; }
};
static request make_request() { return {"SELECT 'abcd'"}; }
};
BOOST_FIXTURE_TEST_CASE(execute_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Execute
conn.execute(make_request(), result, ec, diag);
// Check
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_sync_exc, fixture)
{
conn.execute(make_request(), result);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_diag, fixture)
{
diagnostics diag;
conn.async_execute(make_request(), result, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_nodiag, fixture)
{
conn.async_execute(make_request(), result, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_deferred, fixture)
{
auto op = conn.async_execute(make_request(), result, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Execute
conn.start_execution(make_request(), st, ec, diag);
// Check
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_exc, fixture)
{
conn.start_execution(make_request(), st);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_diag, fixture)
{
diagnostics diag;
conn.async_start_execution(make_request(), st, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_nodiag, fixture)
{
conn.async_start_execution(make_request(), st, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_deferred, fixture)
{
auto op = conn.async_start_execution(make_request(), st, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
// Spotcheck: old connection doesn't generate compile errors.
// Intentionally not run
void old_connection()
{
tcp_connection conn(global_context_executor());
results result;
execution_state st;
error_code ec;
diagnostics diag;
conn.execute(fixture::make_request(), result, ec, diag);
conn.execute(fixture::make_request(), result);
conn.async_execute(fixture::make_request(), result, asio::detached);
conn.async_execute(fixture::make_request(), result, diag, asio::detached);
conn.async_execute(fixture::make_request(), result, diag, asio::deferred)(asio::detached);
conn.start_execution(fixture::make_request(), st, ec, diag);
conn.start_execution(fixture::make_request(), st);
conn.async_start_execution(fixture::make_request(), st, asio::detached);
conn.async_start_execution(fixture::make_request(), st, diag, asio::detached);
conn.async_start_execution(fixture::make_request(), st, diag, asio::deferred)(asio::detached);
}
BOOST_AUTO_TEST_SUITE_END()
// The execution request is forwarded correctly, with the correct value category.
// Lvalues are not moved
BOOST_AUTO_TEST_SUITE(forwarding_lvalues)
struct fixture
{
// A request where we can detect moved-from state.
// std::string move-constructor doesn't offer guarantees about moved-from objects,
// while std::vector does
struct vector_request
{
std::vector<char> buff;
vector_request(string_view v) : buff(v.begin(), v.end()) {}
operator string_view() const { return {buff.data(), buff.size()}; }
};
any_connection conn{create_test_any_connection()};
results result;
execution_state st;
vector_request req{"SELECT 'abcd'"};
fixture() { get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build())); }
void validate_bytes_written()
{
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(
get_stream(conn).bytes_written(),
create_query_frame(0, "SELECT 'abcd'")
);
BOOST_TEST(static_cast<string_view>(req) == "SELECT 'abcd'");
}
};
BOOST_FIXTURE_TEST_CASE(execute_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Execute
conn.execute(req, result, ec, diag);
// Validate
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_sync_exc, fixture)
{
conn.execute(req, result);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_diag, fixture)
{
diagnostics diag;
conn.async_execute(req, result, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_nodiag, fixture)
{
conn.async_execute(req, result, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(execute_async_deferred, fixture)
{
auto op = conn.async_execute(req, result, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_errc, fixture)
{
// Setup
error_code ec;
diagnostics diag;
// Execute
conn.start_execution(req, st, ec, diag);
// Check
BOOST_TEST(ec == error_code());
BOOST_TEST(diag == diagnostics());
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_sync_exc, fixture)
{
conn.start_execution(req, st);
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_diag, fixture)
{
diagnostics diag;
conn.async_start_execution(req, st, diag, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_nodiag, fixture)
{
conn.async_start_execution(req, st, as_netresult).validate_no_error();
validate_bytes_written();
}
BOOST_FIXTURE_TEST_CASE(start_execution_async_deferred, fixture)
{
auto op = conn.async_start_execution(req, st, asio::deferred);
std::move(op)(as_netresult).validate_no_error();
validate_bytes_written();
}
// Spotcheck: old connection doesn't generate compile errors.
// Intentionally not run
void old_connection()
{
tcp_connection conn(global_context_executor());
results result;
execution_state st;
error_code ec;
diagnostics diag;
const fixture::vector_request req{"SELECT 'abc'"};
conn.execute(req, result, ec, diag);
conn.execute(req, result);
conn.async_execute(req, result, asio::detached);
conn.async_execute(req, result, diag, asio::detached);
conn.async_execute(req, result, diag, asio::deferred)(asio::detached);
conn.start_execution(req, st, ec, diag);
conn.start_execution(req, st);
conn.async_start_execution(req, st, asio::detached);
conn.async_start_execution(req, st, diag, asio::detached);
conn.async_start_execution(req, st, diag, asio::deferred)(asio::detached);
}
BOOST_AUTO_TEST_SUITE_END()
// Deferred tokens appropriately decay-copy lvalues
BOOST_AUTO_TEST_SUITE(deferred_tokens_lvalues)
BOOST_AUTO_TEST_CASE(execute)
{
// Setup
auto conn = create_test_any_connection();
results result;
get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build()));
std::string req(128, 'a');
// Create a deferred op
auto op = conn.async_execute(req, result, asio::deferred);
// Mutate the argument
std::fill(req.begin(), req.end(), 'b');
// Initiate
std::move(op)(as_netresult).validate_no_error();
// We wrote the initial value
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(
get_stream(conn).bytes_written(),
create_query_frame(0, std::string(128, 'a'))
);
}
BOOST_AUTO_TEST_CASE(start_execution)
{
// Setup
auto conn = create_test_any_connection();
execution_state st;
get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build()));
std::string req(128, 'a');
// Create a deferred op
auto op = conn.async_start_execution(req, st, asio::deferred);
// Mutate the argument
std::fill(req.begin(), req.end(), 'b');
// Initiate
std::move(op)(as_netresult).validate_no_error();
// We wrote the initial value
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(
get_stream(conn).bytes_written(),
create_query_frame(0, std::string(128, 'a'))
);
}
BOOST_AUTO_TEST_SUITE_END()
// Spotcheck: the types returned by with_params are correct
BOOST_AUTO_TEST_CASE(with_params_types)
{
{
// const references
const std::string s = "abc";
auto p1 = with_params("SELECT {}", s);
static_assert(std::is_same<decltype(p1), with_params_t<std::string>>::value, "");
}
{
// references
std::string s = "abc";
auto p = with_params("SELECT {}", s);
static_assert(std::is_same<decltype(p), with_params_t<std::string>>::value, "");
}
{
// rvalue references
std::string s = "abc";
auto p = with_params("SELECT {}", std::move(s));
static_assert(std::is_same<decltype(p), with_params_t<std::string>>::value, "");
}
{
// pure rvalues
auto p = with_params("SELECT {}", std::string());
static_assert(std::is_same<decltype(p), with_params_t<std::string>>::value, "");
}
{
// std::ref
std::string s = "abc";
auto p = with_params("SELECT {}", std::ref(s));
static_assert(std::is_same<decltype(p), with_params_t<std::string&>>::value, "");
}
{
// std::ref (const)
const std::string s = "abc";
auto p = with_params("SELECT {}", std::ref(s));
static_assert(std::is_same<decltype(p), with_params_t<const std::string&>>::value, "");
}
}
// Regression test: async_execute() doesn't cause side effects in the initiation
BOOST_AUTO_TEST_CASE(async_execute_side_effects_in_initiation)
{
auto conn = create_test_any_connection();
results result1, result2;
// Resultsets will be complete as soon as a message is read
get_stream(conn)
.add_bytes(create_ok_frame(1, ok_builder().affected_rows(2).build()))
.add_bytes(create_ok_frame(1, ok_builder().affected_rows(1).build()));
// Create two queries as deferred objects, but don't run them yet
auto q1 = conn.async_execute("Q1", result1, asio::deferred);
auto q2 = conn.async_execute("Q2", result2, asio::deferred);
// Run them in reverse order
std::move(q2)(as_netresult).validate_no_error();
std::move(q1)(as_netresult).validate_no_error();
// Check that we wrote Q2's message first, then Q1's
auto expected = concat_copy(
create_frame(0, {0x03, 0x51, 0x32}), // query request Q2
create_frame(0, {0x03, 0x51, 0x31}) // query request Q1
);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(get_stream(conn).bytes_written(), expected);
// Check that the results got the right ok_packets
BOOST_TEST(result2.affected_rows() == 2u);
BOOST_TEST(result1.affected_rows() == 1u);
}
// Regression test: bound statements correctly store statement handle and params
// when used with deferred tokens
BOOST_AUTO_TEST_CASE(async_execute_deferred_lifetimes)
{
// Setup
constexpr std::uint8_t expected_msg[] = {
0x15, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74,
};
results result;
auto conn = create_test_any_connection();
get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build()));
// Create a bound statement on the heap. This helps tooling detect memory errors
using bound_stmt_t = bound_statement_tuple<std::tuple<std::string, std::nullptr_t>>;
std::unique_ptr<bound_stmt_t> stmt_ptr{
new bound_stmt_t{statement_builder().id(1).num_params(2).build().bind(std::string("test"), nullptr)}
};
// Deferred op
auto op = conn.async_execute(*stmt_ptr, result, asio::deferred);
// Free the statement
stmt_ptr.reset();
// Actually run the op
std::move(op)(as_netresult).validate_no_error();
// verify that the op had the intended effects
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(get_stream(conn).bytes_written(), expected_msg);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -6,6 +6,7 @@
//
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/blob.hpp>
#include <boost/mysql/column_type.hpp>
#include <boost/mysql/connection.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
@ -42,6 +43,7 @@
#include "test_unit/create_row_message.hpp"
#include "test_unit/create_statement.hpp"
#include "test_unit/fail_count.hpp"
#include "test_unit/test_any_connection.hpp"
#include "test_unit/test_stream.hpp"
using namespace boost::mysql::test;
@ -50,51 +52,17 @@ namespace asio = boost::asio;
BOOST_AUTO_TEST_SUITE(test_misc)
using test_connection = connection<test_stream>;
// Make sure async_execute() and friends don't cause side
// effects in the initiation
BOOST_AUTO_TEST_CASE(async_execute_side_effects_in_initiation)
{
test_connection conn;
results result1, result2;
// Resultsets will be complete as soon as a message is read
conn.stream()
.add_bytes(create_ok_frame(1, ok_builder().affected_rows(2).build()))
.add_bytes(create_ok_frame(1, ok_builder().affected_rows(1).build()));
// Create two queries as deferred objects, but don't run them yet
auto q1 = conn.async_execute("Q1", result1, asio::deferred);
auto q2 = conn.async_execute("Q2", result2, asio::deferred);
// Run them in reverse order
std::move(q2)(as_netresult).validate_no_error();
std::move(q1)(as_netresult).validate_no_error();
// Check that we wrote Q2's message first, then Q1's
auto expected = concat_copy(
create_frame(0, {0x03, 0x51, 0x32}), // query request Q2
create_frame(0, {0x03, 0x51, 0x31}) // query request Q1
);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), expected);
// Check that the results got the right ok_packets
BOOST_TEST(result2.affected_rows() == 2u);
BOOST_TEST(result1.affected_rows() == 1u);
}
// spotcheck for the dynamic interface
// Verifies that execute (dynamic interface) works when rows come in separate batches
// This is testing the interaction between the network algorithm and results
BOOST_AUTO_TEST_CASE(execute_multiple_batches)
{
// Setup
test_connection conn;
auto conn = create_test_any_connection();
results result;
// Message sequence (each on its own read)
conn.stream()
get_stream(conn)
.add_bytes(create_frame(1, {0x02})) // OK, 2 columns
.add_break()
.add_bytes(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef())) // meta
@ -126,7 +94,7 @@ BOOST_AUTO_TEST_CASE(execute_multiple_batches)
// We've written the query request
auto expected_msg = create_frame(0, {0x03, 0x61, 0x62, 0x63}); // ASCII "abc" (plus length)
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), expected_msg);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(get_stream(conn).bytes_written(), expected_msg);
// We've populated the results
BOOST_TEST_REQUIRE(result.size() == 3u);
@ -141,150 +109,20 @@ BOOST_AUTO_TEST_CASE(execute_multiple_batches)
BOOST_TEST(result[2].rows() == makerows(1, "ab"));
}
// Regression check: execute statement with iterator range with a reference type that is convertible to
// field_view, but not equal to field_view
BOOST_AUTO_TEST_CASE(stmt_iterator_reference_not_field_view)
{
results result;
auto stmt = statement_builder().id(1).num_params(2).build();
test_connection conn;
conn.stream().add_bytes(create_ok_frame(1, ok_builder().affected_rows(50).info("1st").build()));
// Call the function
std::vector<field> fields{field_view("test"), field_view()};
conn.execute(stmt.bind(fields.begin(), fields.end()), result);
// Verify the message we sent
constexpr std::uint8_t expected_msg[] = {
0x15, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74,
};
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), expected_msg);
// Verify the results
BOOST_TEST_REQUIRE(result.size() == 1u);
BOOST_TEST(result.meta().size() == 0u);
BOOST_TEST(result.affected_rows() == 50u);
BOOST_TEST(result.info() == "1st");
}
// The serialized form of executing a statement with ID=1, params=("test", nullptr)
constexpr std::uint8_t execute_stmt_msg[] = {
0x15, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x02, 0x01, 0xfe, 0x00, 0x06, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74,
};
// Verify that we correctly perform a decay-copy of the execution request
// in async_execute(), relevant for deferred tokens
BOOST_AUTO_TEST_CASE(async_execute_deferred_lifetimes_rvalues)
{
test_connection conn;
results result;
conn.stream().add_bytes(create_ok_frame(1, ok_builder().info("1st").build()));
// Deferred op. Execution request is a temporary
auto op = conn.async_execute(
statement_builder().id(1).num_params(2).build().bind(std::string("test"), nullptr),
result,
asio::deferred
);
std::move(op)(as_netresult).validate_no_error();
// verify that the op had the intended effects
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), execute_stmt_msg);
BOOST_TEST(result.info() == "1st");
}
BOOST_AUTO_TEST_CASE(async_execute_deferred_lifetimes_lvalues)
{
test_connection conn;
results result;
// Create a bound statement on the heap. This helps tooling detect memory errors
using bound_stmt_t = bound_statement_tuple<std::tuple<std::string, std::nullptr_t>>;
auto stmt = statement_builder().id(1).num_params(2).build();
std::unique_ptr<bound_stmt_t> stmt_ptr{new bound_stmt_t{stmt.bind(std::string("test"), nullptr)}};
// Messages
conn.stream().add_bytes(create_ok_frame(1, ok_builder().info("1st").build()));
// Deferred op
auto op = conn.async_execute(*stmt_ptr, result, asio::deferred);
// Free the statement
stmt_ptr.reset();
// Actually run the op
std::move(op)(as_netresult).validate_no_error();
// verify that the op had the intended effects
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), execute_stmt_msg);
BOOST_TEST(result.info() == "1st");
}
// Verify that we correctly perform a decay-copy of the parameters and the
// statement handle for async_start_execution(), relevant for deferred tokens
BOOST_AUTO_TEST_CASE(async_start_execution_deferred_lifetimes_rvalues)
{
test_connection conn;
execution_state st;
conn.stream().add_bytes(create_ok_frame(1, ok_builder().info("1st").build()));
// Deferred op. Execution request is a temporary
auto op = conn.async_start_execution(
statement_builder().id(1).num_params(2).build().bind(std::string("test"), nullptr),
st,
asio::deferred
);
std::move(op)(as_netresult).validate_no_error();
// verify that the op had the intended effects
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), execute_stmt_msg);
BOOST_TEST(st.info() == "1st");
}
BOOST_AUTO_TEST_CASE(deferred_lifetimes_statement)
{
test_connection conn;
execution_state st;
conn.stream().add_bytes(create_ok_frame(1, ok_builder().info("1st").build()));
// Create a bound statement on the heap. This helps tooling detect memory errors
using bound_stmt_t = bound_statement_tuple<std::tuple<std::string, std::nullptr_t>>;
auto stmt = statement_builder().id(1).num_params(2).build();
std::unique_ptr<bound_stmt_t> stmt_ptr{new bound_stmt_t{stmt.bind(std::string("test"), nullptr)}};
// Deferred op
auto op = conn.async_start_execution(*stmt_ptr, st, asio::deferred);
// Free the statement
stmt_ptr.reset();
// Actually run the op
std::move(op)(as_netresult).validate_no_error();
// verify that the op had the intended effects
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), execute_stmt_msg);
BOOST_TEST(st.info() == "1st");
}
// Verify that async_close_statement doesn't require the passed-in statement to be alive. Only
// relevant for deferred tokens.
// Regression check: async_close_statement doesn't require the passed-in statement to be alive
// when used with deferred tokens.
BOOST_AUTO_TEST_CASE(async_close_statement_handle_deferred_tokens)
{
test_connection conn;
auto stmt = statement_builder().id(3).build();
conn.stream().add_bytes(create_ok_frame(1, ok_builder().build()));
// Setup
auto conn = create_test_any_connection();
std::unique_ptr<statement> stmt{new statement(statement_builder().id(3).build())};
get_stream(conn).add_bytes(create_ok_frame(1, ok_builder().build()));
// Deferred op
auto op = conn.async_close_statement(stmt, asio::deferred);
auto op = conn.async_close_statement(*stmt, asio::deferred);
// Invalidate the original variable
stmt = statement_builder().id(42).build();
stmt.reset();
// Run the operation
std::move(op)(as_netresult).validate_no_error();
@ -294,21 +132,21 @@ BOOST_AUTO_TEST_CASE(async_close_statement_handle_deferred_tokens)
create_frame(0, {0x19, 0x03, 0x00, 0x00, 0x00}),
create_frame(0, {0x0e})
);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(conn.stream().bytes_written(), expected_message);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(get_stream(conn).bytes_written(), expected_message);
}
// Regression check: when there is a network error, sync functions
// returning a value fail with an assertion
BOOST_AUTO_TEST_CASE(net_error_prepare_statement)
{
using netmaker_stmt = netfun_maker<statement, test_connection, string_view>;
using netmaker_stmt = netfun_maker<statement, any_connection, string_view>;
struct
{
const char* name;
netmaker_stmt::signature prepare_statement;
} fns[] = {
{"sync", netmaker_stmt::sync_errc(&test_connection::prepare_statement) },
{"async", netmaker_stmt::async_diag(&test_connection::async_prepare_statement)},
{"sync", netmaker_stmt::sync_errc(&any_connection::prepare_statement) },
{"async", netmaker_stmt::async_diag(&any_connection::async_prepare_statement)},
};
for (const auto& fn : fns)
@ -316,8 +154,8 @@ BOOST_AUTO_TEST_CASE(net_error_prepare_statement)
BOOST_TEST_CONTEXT(fn.name)
{
// Setup
test_connection conn;
conn.stream().set_fail_count(fail_count(0, boost::asio::error::connection_reset));
auto conn = create_test_any_connection();
get_stream(conn).set_fail_count(fail_count(0, boost::asio::error::connection_reset));
fn.prepare_statement(conn, "SELECT 1").validate_error(boost::asio::error::connection_reset);
}
@ -326,14 +164,14 @@ BOOST_AUTO_TEST_CASE(net_error_prepare_statement)
BOOST_AUTO_TEST_CASE(net_error_read_some_rows)
{
using netmaker_stmt = netfun_maker<rows_view, test_connection, execution_state&>;
using netmaker_stmt = netfun_maker<rows_view, any_connection, execution_state&>;
struct
{
const char* name;
netmaker_stmt::signature read_some_rows;
} fns[] = {
{"sync", netmaker_stmt::sync_errc(&test_connection::read_some_rows) },
{"async", netmaker_stmt::async_diag(&test_connection::async_read_some_rows)},
{"sync", netmaker_stmt::sync_errc(&any_connection::read_some_rows) },
{"async", netmaker_stmt::async_diag(&any_connection::async_read_some_rows)},
};
for (const auto& fn : fns)
@ -341,8 +179,8 @@ BOOST_AUTO_TEST_CASE(net_error_read_some_rows)
BOOST_TEST_CONTEXT(fn.name)
{
// Setup
test_connection conn;
conn.stream().set_fail_count(fail_count(0, boost::asio::error::connection_reset));
auto conn = create_test_any_connection();
get_stream(conn).set_fail_count(fail_count(0, boost::asio::error::connection_reset));
execution_state st;
add_meta(get_iface(st), {column_type::bigint});
@ -353,14 +191,14 @@ BOOST_AUTO_TEST_CASE(net_error_read_some_rows)
BOOST_AUTO_TEST_CASE(net_error_void_signature)
{
using netmaker_execute = netfun_maker<void, test_connection, const string_view&, results&>;
using netmaker_execute = netfun_maker<void, any_connection, const string_view&, results&>;
struct
{
const char* name;
netmaker_execute::signature execute;
} fns[] = {
{"sync", netmaker_execute::sync_errc(&test_connection::execute) },
{"async", netmaker_execute::async_diag(&test_connection::async_execute)},
{"sync", netmaker_execute::sync_errc(&any_connection::execute) },
{"async", netmaker_execute::async_diag(&any_connection::async_execute)},
};
for (const auto& fn : fns)
@ -368,8 +206,8 @@ BOOST_AUTO_TEST_CASE(net_error_void_signature)
BOOST_TEST_CONTEXT(fn.name)
{
// Setup
test_connection conn;
conn.stream().set_fail_count(fail_count(0, boost::asio::error::connection_reset));
auto conn = create_test_any_connection();
get_stream(conn).set_fail_count(fail_count(0, boost::asio::error::connection_reset));
results r;
fn.execute(conn, "SELECT 1", r).validate_error(boost::asio::error::connection_reset);
@ -377,74 +215,35 @@ BOOST_AUTO_TEST_CASE(net_error_void_signature)
}
}
// We can bind and execute statements using references
BOOST_AUTO_TEST_CASE(stmt_tuple_ref)
{
// Setup
std::string s = "abcdef";
blob b{0x00, 0x01, 0x02};
results r;
auto stmt = statement_builder().id(1).num_params(2).build();
// Connection
test_connection conn;
conn.stream().add_bytes(create_ok_frame(1, ok_builder().build()));
// This should compile and run
BOOST_CHECK_NO_THROW(conn.execute(stmt.bind(std::ref(s), std::ref(b)), r));
}
// Helper to create any_connection objects using test_stream
struct test_any_connection_fixture
{
any_connection conn;
test_any_connection_fixture()
: conn(detail::access::construct<any_connection>(
default_initial_read_buffer_size,
static_cast<std::size_t>(-1), // no buffer limit
std::unique_ptr<detail::engine>(
new detail::engine_impl<detail::engine_stream_adaptor<test_stream>>()
)
))
{
}
test_stream& stream()
{
return detail::stream_from_engine<test_stream>(detail::access::get_impl(conn).get_engine());
}
};
auto pipeline_fn = netfun_maker<void, any_connection, const pipeline_request&, std::vector<stage_response>&>::
async_diag(&any_connection::async_run_pipeline);
// empty pipelines complete immediately, posting adequately
BOOST_FIXTURE_TEST_CASE(empty_pipeline, test_any_connection_fixture)
BOOST_AUTO_TEST_CASE(empty_pipeline)
{
// Setup
auto conn = create_test_any_connection();
pipeline_request req;
std::vector<stage_response> res;
// Run it. It should complete immediately, posting to the correct executor (verified by the testing
// infrastructure)
pipeline_fn(conn, req, res).validate_no_error();
conn.async_run_pipeline(req, res, as_netresult).validate_no_error();
BOOST_TEST(res.size() == 0u);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(get_stream(conn).bytes_written(), blob{});
}
// fatal errors in pipelines behave correctly
BOOST_FIXTURE_TEST_CASE(pipeline_fatal_error, test_any_connection_fixture)
BOOST_AUTO_TEST_CASE(pipeline_fatal_error)
{
// Setup
auto conn = create_test_any_connection();
pipeline_request req;
std::vector<stage_response> res;
req.add_execute("SELECT 1").add_execute("SELECT 2");
// The first read will fail
stream().set_fail_count(fail_count(1, boost::asio::error::network_reset));
get_stream(conn).set_fail_count(fail_count(1, boost::asio::error::network_reset));
// Run it
pipeline_fn(conn, req, res).validate_error(boost::asio::error::network_reset);
conn.async_run_pipeline(req, res, as_netresult).validate_error(boost::asio::error::network_reset);
// Validate the results
BOOST_TEST(res.size() == 2u);

View File

@ -8,21 +8,22 @@
// Since integration tests can't reliably test multifunction operations
// that span over multiple messages, we test the complete multifn fllow in this unit tests.
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/column_type.hpp>
#include <boost/mysql/connection.hpp>
#include <boost/mysql/execution_state.hpp>
#include <boost/mysql/rows_view.hpp>
#include <boost/test/unit_test.hpp>
#include "test_common/check_meta.hpp"
#include "test_common/netfun_maker.hpp"
#include "test_common/network_result.hpp"
#include "test_unit/create_coldef_frame.hpp"
#include "test_unit/create_frame.hpp"
#include "test_unit/create_meta.hpp"
#include "test_unit/create_ok.hpp"
#include "test_unit/create_ok_frame.hpp"
#include "test_unit/create_row_message.hpp"
#include "test_unit/test_any_connection.hpp"
#include "test_unit/test_stream.hpp"
using namespace boost::mysql;
@ -30,188 +31,143 @@ using namespace boost::mysql::test;
BOOST_AUTO_TEST_SUITE(test_multifn)
using test_connection = connection<test_stream>;
using start_query_netm = netfun_maker<void, test_connection, const string_view&, execution_state&>;
using read_resultset_head_netm = netfun_maker<void, test_connection, execution_state&>;
using read_some_rows_netm = netfun_maker<rows_view, test_connection, execution_state&>;
struct
{
start_query_netm::signature start_execution;
read_resultset_head_netm::signature read_resultset_head;
read_some_rows_netm::signature read_some_rows;
const char* name;
} all_fns[] = {
{start_query_netm::sync_errc(&test_connection::start_execution),
read_resultset_head_netm::sync_errc(&test_connection::read_resultset_head),
read_some_rows_netm::sync_errc(&test_connection::read_some_rows),
"sync" },
{start_query_netm::async_diag(&test_connection::async_start_execution),
read_resultset_head_netm::async_diag(&test_connection::async_read_resultset_head),
read_some_rows_netm::async_diag(&test_connection::async_read_some_rows),
"async"},
};
BOOST_AUTO_TEST_CASE(separate_batches)
{
for (auto fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
execution_state st;
test_connection conn;
conn.stream()
.add_bytes(create_frame(1, {0x01}))
.add_break()
.add_bytes(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef()))
.add_break()
.add_bytes(create_text_row_message(3, "abc"))
.add_break()
.add_bytes(create_eof_frame(
4,
ok_builder().affected_rows(10u).info("1st").more_results(true).build()
))
.add_break()
.add_bytes(create_frame(5, {0x01}))
.add_break()
.add_bytes(create_coldef_frame(6, meta_builder().type(column_type::decimal).build_coldef()))
.add_break()
.add_bytes(create_text_row_message(7, "ab"))
.add_bytes(create_text_row_message(8, "plo"))
.add_break()
.add_bytes(create_text_row_message(9, "hju"))
.add_bytes(create_eof_frame(10, ok_builder().affected_rows(30u).info("2nd").build()));
execution_state st;
auto conn = create_test_any_connection();
get_stream(conn)
.add_bytes(create_frame(1, {0x01}))
.add_break()
.add_bytes(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef()))
.add_break()
.add_bytes(create_text_row_message(3, "abc"))
.add_break()
.add_bytes(create_eof_frame(4, ok_builder().affected_rows(10u).info("1st").more_results(true).build())
)
.add_break()
.add_bytes(create_frame(5, {0x01}))
.add_break()
.add_bytes(create_coldef_frame(6, meta_builder().type(column_type::decimal).build_coldef()))
.add_break()
.add_bytes(create_text_row_message(7, "ab"))
.add_bytes(create_text_row_message(8, "plo"))
.add_break()
.add_bytes(create_text_row_message(9, "hju"))
.add_bytes(create_eof_frame(10, ok_builder().affected_rows(30u).info("2nd").build()));
// Start
fns.start_execution(conn, "SELECT 1", st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::varchar});
// Start
conn.async_start_execution("SELECT 1", st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::varchar});
// 1st resultset, row
auto rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.should_read_rows());
BOOST_TEST(rv == makerows(1, "abc"));
// 1st resultset, row
auto rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.should_read_rows());
BOOST_TEST(rv == makerows(1, "abc"));
// 1st resultset, eof
rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(rv == makerows(1));
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// 1st resultset, eof
rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(rv == makerows(1));
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// 2nd resultset, head
fns.read_resultset_head(conn, st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::decimal});
// 2nd resultset, head
conn.async_read_resultset_head(st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::decimal});
// 2nd resultset, row batch
rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.should_read_rows());
BOOST_TEST(rv == makerows(1, "ab", "plo"));
// 2nd resultset, row batch
rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.should_read_rows());
BOOST_TEST(rv == makerows(1, "ab", "plo"));
// 2nd resultset, last row & eof
rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(rv == makerows(1, "hju"));
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "2nd");
}
}
// 2nd resultset, last row & eof
rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(rv == makerows(1, "hju"));
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "2nd");
}
// The server sent us a single, big message with everything
BOOST_AUTO_TEST_CASE(single_read)
{
for (auto fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
execution_state st;
test_connection conn(buffer_params(4096));
execution_state st;
any_connection_params params;
params.initial_buffer_size = 4096;
auto conn = create_test_any_connection(params);
conn.stream()
.add_bytes(create_frame(1, {0x01}))
.add_bytes(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef()))
.add_bytes(create_text_row_message(3, "abc"))
.add_bytes(create_eof_frame(
4,
ok_builder().affected_rows(10u).info("1st").more_results(true).build()
))
.add_bytes(create_frame(5, {0x01}))
.add_bytes(create_coldef_frame(6, meta_builder().type(column_type::decimal).build_coldef()))
.add_bytes(create_text_row_message(7, "ab"))
.add_bytes(create_text_row_message(8, "plo"))
.add_bytes(create_text_row_message(9, "hju"))
.add_bytes(create_eof_frame(10, ok_builder().affected_rows(30u).info("2nd").build()));
get_stream(conn)
.add_bytes(create_frame(1, {0x01}))
.add_bytes(create_coldef_frame(2, meta_builder().type(column_type::varchar).build_coldef()))
.add_bytes(create_text_row_message(3, "abc"))
.add_bytes(create_eof_frame(4, ok_builder().affected_rows(10u).info("1st").more_results(true).build())
)
.add_bytes(create_frame(5, {0x01}))
.add_bytes(create_coldef_frame(6, meta_builder().type(column_type::decimal).build_coldef()))
.add_bytes(create_text_row_message(7, "ab"))
.add_bytes(create_text_row_message(8, "plo"))
.add_bytes(create_text_row_message(9, "hju"))
.add_bytes(create_eof_frame(10, ok_builder().affected_rows(30u).info("2nd").build()));
// Start
fns.start_execution(conn, "SELECT 1", st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::varchar});
// Start
conn.async_start_execution("SELECT 1", st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::varchar});
// First resultset
auto rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(rv == makerows(1, "abc"));
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// First resultset
auto rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(rv == makerows(1, "abc"));
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// 2nd resultset, head
fns.read_resultset_head(conn, st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::decimal});
// 2nd resultset, head
conn.async_read_resultset_head(st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_rows());
check_meta(st.meta(), {column_type::decimal});
// 2nd resultset
rv = fns.read_some_rows(conn, st).get();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(rv == makerows(1, "ab", "plo", "hju"));
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "2nd");
}
}
// 2nd resultset
rv = conn.async_read_some_rows(st, as_netresult).get();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(rv == makerows(1, "ab", "plo", "hju"));
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "2nd");
}
BOOST_AUTO_TEST_CASE(empty_resultsets)
{
for (auto fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
execution_state st;
test_connection conn(buffer_params(4096));
execution_state st;
any_connection_params params;
params.initial_buffer_size = 4096;
auto conn = create_test_any_connection(params);
conn.stream()
.add_bytes(
create_ok_frame(1, ok_builder().affected_rows(10u).info("1st").more_results(true).build())
)
.add_bytes(
create_ok_frame(2, ok_builder().affected_rows(20u).info("2nd").more_results(true).build())
)
.add_bytes(create_ok_frame(3, ok_builder().affected_rows(30u).info("3rd").build()));
get_stream(conn)
.add_bytes(create_ok_frame(1, ok_builder().affected_rows(10u).info("1st").more_results(true).build()))
.add_bytes(create_ok_frame(2, ok_builder().affected_rows(20u).info("2nd").more_results(true).build()))
.add_bytes(create_ok_frame(3, ok_builder().affected_rows(30u).info("3rd").build()));
// Start
fns.start_execution(conn, "SELECT 1", st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// Start
conn.async_start_execution("SELECT 1", st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 10u);
BOOST_TEST(st.info() == "1st");
// 2nd resultset
fns.read_resultset_head(conn, st).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 20u);
BOOST_TEST(st.info() == "2nd");
// 2nd resultset
conn.async_read_resultset_head(st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.should_read_head());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 20u);
BOOST_TEST(st.info() == "2nd");
// 3rd resultset
fns.read_resultset_head(conn, st).validate_no_error();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "3rd");
}
}
// 3rd resultset
conn.async_read_resultset_head(st, as_netresult).validate_no_error();
BOOST_TEST_REQUIRE(st.complete());
BOOST_TEST(st.meta().size() == 0u);
BOOST_TEST(st.affected_rows() == 30u);
BOOST_TEST(st.info() == "3rd");
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -7,23 +7,22 @@
#include <boost/mysql/detail/config.hpp>
#include "test_unit/test_stream.hpp"
#ifdef BOOST_MYSQL_CXX14
#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/connection.hpp>
#include <boost/mysql/static_execution_state.hpp>
#include <boost/core/span.hpp>
#include <boost/test/unit_test.hpp>
#include "test_common/netfun_maker.hpp"
#include "test_common/network_result.hpp"
#include "test_unit/create_execution_processor.hpp"
#include "test_unit/create_frame.hpp"
#include "test_unit/create_meta.hpp"
#include "test_unit/create_ok.hpp"
#include "test_unit/create_row_message.hpp"
#include "test_unit/test_any_connection.hpp"
#include "test_unit/test_stream.hpp"
using namespace boost::mysql::test;
using namespace boost::mysql;
@ -31,37 +30,22 @@ using boost::span;
BOOST_AUTO_TEST_SUITE(test_read_some_rows_static)
using test_connection = connection<test_stream>;
using row1 = std::tuple<int, float>;
using row2 = std::tuple<double>;
using state_t = static_execution_state<row1, row1, row2, row1, row2>;
using netfun_maker_row1 = netfun_maker<std::size_t, test_connection, state_t&, span<row1> >;
using netfun_maker_row2 = netfun_maker<std::size_t, test_connection, state_t&, span<row2> >;
struct
{
typename netfun_maker_row1::signature read_some_rows_row1;
typename netfun_maker_row2::signature read_some_rows_row2;
const char* name;
} all_fns[] = {
{netfun_maker_row1::sync_errc(&test_connection::read_some_rows),
netfun_maker_row2::sync_errc(&test_connection::read_some_rows),
"sync" },
{netfun_maker_row1::async_diag(&test_connection::async_read_some_rows),
netfun_maker_row2::async_diag(&test_connection::async_read_some_rows),
"async"},
};
struct fixture
{
state_t st;
test_connection conn;
any_connection conn{create_test_any_connection()};
std::array<row1, 3> storage1;
std::array<row2, 3> storage2;
test_stream& stream() noexcept { return conn.stream(); }
span<row1> storage1_span() { return storage1; }
span<row2> storage2_span() { return storage2; }
test_stream& stream() noexcept { return get_stream(conn); }
void add_ok() { ::add_ok(get_iface(st), ok_builder().more_results(true).build()); }
@ -87,99 +71,83 @@ struct fixture
}
};
BOOST_AUTO_TEST_CASE(repeated_row_types)
BOOST_FIXTURE_TEST_CASE(repeated_row_types, fixture)
{
for (const auto& fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
fixture fix;
fix.add_meta_row1();
add_meta_row1();
// 1st resultset: row1
fix.stream()
.add_bytes(create_text_row_message(0, 10, 4.2f))
.add_bytes(create_text_row_message(1, 11, 4.3f));
// 1st resultset: row1
stream().add_bytes(create_text_row_message(0, 10, 4.2f)).add_bytes(create_text_row_message(1, 11, 4.3f));
std::size_t num_rows = fns.read_some_rows_row1(fix.conn, fix.st, fix.storage1).get();
BOOST_TEST_REQUIRE(num_rows == 2u);
BOOST_TEST((fix.storage1[0] == row1{10, 4.2f}));
BOOST_TEST((fix.storage1[1] == row1{11, 4.3f}));
std::size_t num_rows = conn.async_read_some_rows(st, storage1_span(), as_netresult).get();
BOOST_TEST_REQUIRE(num_rows == 2u);
BOOST_TEST((storage1[0] == row1{10, 4.2f}));
BOOST_TEST((storage1[1] == row1{11, 4.3f}));
// Advance resultset
fix.add_ok();
fix.add_meta_row1();
BOOST_TEST_REQUIRE(fix.st.should_read_rows());
// Advance resultset
add_ok();
add_meta_row1();
BOOST_TEST_REQUIRE(st.should_read_rows());
// 2nd resultset: row1 again
fix.stream().add_bytes(create_text_row_message(2, 13, 0.2f));
num_rows = fns.read_some_rows_row1(fix.conn, fix.st, fix.storage1).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((fix.storage1[0] == row1{13, 0.2f}));
// 2nd resultset: row1 again
stream().add_bytes(create_text_row_message(2, 13, 0.2f));
num_rows = conn.async_read_some_rows(st, storage1_span(), as_netresult).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((storage1[0] == row1{13, 0.2f}));
// Advance resultset
fix.add_ok();
fix.add_meta_row2();
BOOST_TEST_REQUIRE(fix.st.should_read_rows());
// Advance resultset
add_ok();
add_meta_row2();
BOOST_TEST_REQUIRE(st.should_read_rows());
// 3rd resultset: row2
fix.stream().add_bytes(create_text_row_message(3, 9.1));
num_rows = fns.read_some_rows_row2(fix.conn, fix.st, fix.storage2).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((fix.storage2[0] == row2{9.1}));
// 3rd resultset: row2
stream().add_bytes(create_text_row_message(3, 9.1));
num_rows = conn.async_read_some_rows(st, storage2_span(), as_netresult).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((storage2[0] == row2{9.1}));
// Advance resultset
fix.add_ok();
fix.add_meta_row1();
BOOST_TEST_REQUIRE(fix.st.should_read_rows());
// Advance resultset
add_ok();
add_meta_row1();
BOOST_TEST_REQUIRE(st.should_read_rows());
// 4th resultset: row1
fix.stream().add_bytes(create_text_row_message(4, 43, 0.7f));
num_rows = fns.read_some_rows_row1(fix.conn, fix.st, fix.storage1).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((fix.storage1[0] == row1{43, 0.7f}));
// 4th resultset: row1
stream().add_bytes(create_text_row_message(4, 43, 0.7f));
num_rows = conn.async_read_some_rows(st, storage1_span(), as_netresult).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((storage1[0] == row1{43, 0.7f}));
// Advance resultset
fix.add_ok();
fix.add_meta_row2();
BOOST_TEST_REQUIRE(fix.st.should_read_rows());
// Advance resultset
add_ok();
add_meta_row2();
BOOST_TEST_REQUIRE(st.should_read_rows());
// 5th resultset: row2
fix.stream().add_bytes(create_text_row_message(5, 99.9));
num_rows = fns.read_some_rows_row2(fix.conn, fix.st, fix.storage2).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((fix.storage2[0] == row2{99.9}));
}
}
// 5th resultset: row2
stream().add_bytes(create_text_row_message(5, 99.9));
num_rows = conn.async_read_some_rows(st, storage2_span(), as_netresult).get();
BOOST_TEST_REQUIRE(num_rows == 1u);
BOOST_TEST((storage2[0] == row2{99.9}));
}
BOOST_AUTO_TEST_CASE(error_row_type_mismatch)
BOOST_FIXTURE_TEST_CASE(error_row_type_mismatch, fixture)
{
for (const auto& fns : all_fns)
{
BOOST_TEST_CONTEXT(fns.name)
{
fixture fix;
fix.add_meta_row1();
add_meta_row1();
// 1st resultset: row1. Note that this will consume the message
fix.stream().add_bytes(create_text_row_message(0, 10, 4.2f));
fns.read_some_rows_row2(fix.conn, fix.st, fix.storage2)
.validate_error(client_errc::row_type_mismatch);
// 1st resultset: row1. Note that this will consume the message
stream().add_bytes(create_text_row_message(0, 10, 4.2f));
conn.async_read_some_rows(st, storage2_span(), as_netresult)
.validate_error(client_errc::row_type_mismatch);
// Advance resultset
fix.add_ok();
fix.add_meta_row1();
fix.add_ok();
fix.add_meta_row2();
BOOST_TEST_REQUIRE(fix.st.should_read_rows());
// Advance resultset
add_ok();
add_meta_row1();
add_ok();
add_meta_row2();
BOOST_TEST_REQUIRE(st.should_read_rows());
// 3rd resultset: row2
fix.stream().add_bytes(create_text_row_message(1, 9.1));
fns.read_some_rows_row1(fix.conn, fix.st, fix.storage1)
.validate_error(client_errc::row_type_mismatch);
}
}
// 3rd resultset: row2
stream().add_bytes(create_text_row_message(1, 9.1));
conn.async_read_some_rows(st, storage1_span(), as_netresult)
.validate_error(client_errc::row_type_mismatch);
}
BOOST_AUTO_TEST_SUITE_END()