Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is the documentation for a snapshot of the develop branch, built from commit 2c91289918.
PrevUpHomeNext

(Experimental) Dynamic filters using client-side query formatting

This example demonstrates how to use format_sql_to to implement filters that can be enabled dynamically.

The example employs async functions with stackful coroutines.

This example assumes you have gone through the setup.

// Uses client-side SQL formatting to implement a dynamic filter.
// If you're implementing a filter with many options that can be
// conditionally enabled, this pattern may be useful for you.
//
// Client-side SQL formatting is an experimental feature.

#include <boost/mysql/any_connection.hpp>
#include <boost/mysql/character_set.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/row_view.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_diagnostics.hpp>

#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/optional/optional.hpp>

#include <cassert>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

using boost::mysql::field_view;
using boost::mysql::string_view;
using boost::mysql::with_diagnostics;

// Prints an employee row to stdout
void print_employee(boost::mysql::row_view employee)
{
    std::cout << "id: " << employee.at(0)                             // field 0: id
              << ", first_name: " << std::setw(16) << employee.at(1)  // field 1: first_name
              << ", last_name: " << std::setw(16) << employee.at(2)   // field 2: last_name
              << ", company_id: " << employee.at(3)                   // field 3: company_id
              << ", salary: " << employee.at(4) << '\n';              // field 4: salary
}

// An operator to use in a filter
enum class op_type
{
    lt,   // <
    lte,  // <=
    eq,   // =
    gt,   // >
    gte,  // >=
};

// Returns the SQL operator for the given op_type
string_view op_type_to_sql(op_type value)
{
    switch (value)
    {
    case op_type::lt: return "<";
    case op_type::lte: return "<=";
    case op_type::eq: return "=";
    case op_type::gte: return ">=";
    case op_type::gt: return ">";
    default: assert(false); return "=";
    }
}

// An individual filter to apply.
// For example, filter{"salary", op_type::gt, field_view(20000)} should generate a
// `salary` > 20000 condition
struct filter
{
    string_view field_name;  // The database column name
    op_type op;              // The operator to apply
    field_view field_value;  // The value to check. field_view can hold any MySQL type
};

// Command line arguments
struct cmdline_args
{
    // MySQL username to use during authentication.
    string_view username;

    // MySQL password to use during authentication.
    string_view password;

    // Hostname where the MySQL server is listening.
    string_view server_hostname;

    // The filters to apply
    std::vector<filter> filts;

    // If order_by.has_value(), order employees using the given field
    boost::optional<string_view> order_by;
};

// Parses the command line
static cmdline_args parse_cmdline_args(int argc, char** argv)
{
    // Available options
    const string_view company_id_prefix = "--company-id=";
    const string_view first_name_prefix = "--first-name=";
    const string_view last_name_prefix = "--last-name=";
    const string_view min_salary_prefix = "--min-salary=";
    const string_view order_by_prefix = "--order-by=";

    // Helper function to print the usage message and exit
    auto print_usage_and_exit = [argv]() {
        std::cerr << "Usage: " << argv[0] << " <username> <password> <server-hostname> [filters]\n";
        exit(1);
    };

    // Check number of arguments
    if (argc <= 4)
        print_usage_and_exit();

    // Parse the required arguments
    cmdline_args res;
    res.username = argv[1];
    res.password = argv[2];
    res.server_hostname = argv[3];

    // Parse the filters
    for (int i = 4; i < argc; ++i)
    {
        string_view arg = argv[i];

        // Attempt to match the argument against each prefix
        if (arg.starts_with(company_id_prefix))
        {
            auto value = arg.substr(company_id_prefix.size());
            res.filts.push_back({"company_id", op_type::eq, field_view(value)});
        }
        else if (arg.starts_with(first_name_prefix))
        {
            auto value = arg.substr(first_name_prefix.size());
            res.filts.push_back({"first_name", op_type::eq, field_view(value)});
        }
        else if (arg.starts_with(last_name_prefix))
        {
            auto value = arg.substr(last_name_prefix.size());
            res.filts.push_back({"last_name", op_type::eq, field_view(value)});
        }
        else if (arg.starts_with(min_salary_prefix))
        {
            auto value = std::stod(arg.substr(min_salary_prefix.size()));
            res.filts.push_back({"salary", op_type::gte, field_view(value)});
        }
        else if (arg.starts_with(order_by_prefix))
        {
            auto field_name = arg.substr(order_by_prefix.size());

            // For security, validate the passed field against a set of whitelisted fields
            if (field_name != "id" && field_name != "first_name" && field_name != "last_name" &&
                field_name != "salary")
            {
                std::cerr << "Order-by: invalid field " << field_name << std::endl;
                print_usage_and_exit();
            }
            res.order_by = field_name;
        }
        else
        {
            std::cerr << "Unrecognized option: " << arg << std::endl;
            print_usage_and_exit();
        }
    }

    // We should have at least one filter
    if (res.filts.empty())
    {
        std::cerr << "At least one filter should be specified" << std::endl;
        print_usage_and_exit();
    }

    return res;
}

// 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,
    boost::optional<string_view> order_by
)
{
    // A format context allows composing queries incrementally.
    // This is required because we need to add the ORDER BY clause conditionally
    boost::mysql::format_context ctx(opts);

    // Adds an individual filter to the context. Used by sequence()
    auto filter_format_fn = [](filter item, boost::mysql::format_context_base& elm_ctx) {
        // {:i} formats a string as a SQL identifier. {:r} outputs raw SQL.
        // filter{"key", op_type::eq, field_view(42)} would get formatted as "`key` = 42"
        boost::mysql::format_sql_to(
            elm_ctx,
            "{:i} {:r} {}",
            item.field_name,
            op_type_to_sql(item.op),
            item.field_value
        );
    };

    // Add the query with the filters to ctx.
    // sequence() will invoke filter_format_fn for each element in filts,
    // using the string " AND " as glue, to separate filters
    boost::mysql::format_sql_to(
        ctx,
        "SELECT id, first_name, last_name, company_id, salary FROM employee WHERE {}",
        boost::mysql::sequence(filts, filter_format_fn, " AND ")
    );

    // Add the order by
    if (order_by)
    {
        // identifier formats a string as a SQL identifier, instead of a string literal.
        // For instance, this may generate "ORDER BY `first_name`"
        boost::mysql::format_sql_to(ctx, " ORDER BY {:i}", *order_by);
    }

    // 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();
}

void main_impl(int argc, char** argv)
{
    // Parse the command line
    cmdline_args args = parse_cmdline_args(argc, argv);

    // Create an I/O context, required by all I/O objects
    boost::asio::io_context ctx;

    /**
     * Spawn a stackful coroutine using boost::asio::spawn.
     * The coroutine suspends every time we call an asynchronous function, and
     * will resume when it completes.
     * Note that client-side SQL formatting can be used with both sync and async functions.
     */
    boost::asio::spawn(
        ctx.get_executor(),
        [args](boost::asio::yield_context yield) {
            // Create a connection. Note that client-side SQL formatting
            // requires us to use the newer any_connection.
            boost::mysql::any_connection conn(yield.get_executor());

            // Connection configuration. By default, connections use the utf8mb4 character set
            // (MySQL's name for regular UTF-8).
            boost::mysql::connect_params params;
            params.server_address.emplace_host_and_port(args.server_hostname);
            params.username = args.username;
            params.password = args.password;
            params.database = "boost_mysql_examples";

            // Connect to the server.with_diagnostics will turn any thrown exceptions
            // into error_with_diagnostics, which contain more info than regular exceptions
            conn.async_connect(params, with_diagnostics(yield));

            // Compose the query. format_opts() returns a system::result<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 async_set_character_set if this happens.
            std::string query = compose_get_employees_query(
                conn.format_opts().value(),
                args.filts,
                args.order_by
            );

            // Execute the query as usual. Note that, unlike with prepared statements,
            // formatting happened in the client, and not in the server.
            // Casting to string_view saves a copy in async_execute
            boost::mysql::results result;
            conn.async_execute(string_view(query), result, with_diagnostics(yield));

            // Print the employees
            for (boost::mysql::row_view employee : result.rows())
            {
                print_employee(employee);
            }

            // Notify the MySQL server we want to quit, then close the underlying connection.
            conn.async_close(with_diagnostics(yield));
        },
        // If any exception is thrown in the coroutine body, rethrow it.
        [](std::exception_ptr ptr) {
            if (ptr)
            {
                std::rethrow_exception(ptr);
            }
        }
    );

    // Don't forget to call run()! Otherwise, your program will do nothing.
    ctx.run();
}

int main(int argc, char** argv)
{
    try
    {
        main_impl(argc, argv);
    }
    catch (const boost::mysql::error_with_diagnostics& err)
    {
        // You will only get this type of exceptions if you use with_diagnostics.
        // Some errors include additional diagnostics, like server-provided error messages.
        // Security note: diagnostics::server_message may contain user-supplied values (e.g. the
        // field value that caused the error) and is encoded using to the connection's encoding
        // (UTF-8 by default). Treat is as untrusted input.
        std::cerr << "Error: " << err.what() << '\n'
                  << "Server diagnostics: " << err.get_diagnostics().server_message() << std::endl;
        return 1;
    }
    catch (const std::exception& err)
    {
        std::cerr << "Error: " << err.what() << std::endl;
        return 1;
    }
}

PrevUpHomeNext