Use templates to make writing data to PostgreSQL more generic

This way we don't need to hardwire any types.

Also adds some docs and tests.
HEAD
Jochen Topf 2022-03-07 14:58:27 +01:00
parent ce062967c9
commit 47fc05e286
3 changed files with 155 additions and 34 deletions

View File

@ -167,29 +167,6 @@ pg_conn_t::exec_prepared_internal(char const *stmt, int num_params,
return res;
}
pg_result_t pg_conn_t::exec_prepared(char const *stmt, char const *p1, char const *p2) const
{
std::array<const char *, 2> params{{p1, p2}};
return exec_prepared_internal(stmt, params.size(), params.data());
}
pg_result_t pg_conn_t::exec_prepared(char const *stmt, char const *param) const
{
return exec_prepared_internal(stmt, 1, &param);
}
pg_result_t pg_conn_t::exec_prepared(char const *stmt,
std::string const &param) const
{
return exec_prepared(stmt, param.c_str());
}
pg_result_t pg_conn_t::exec_prepared(char const *stmt, osmid_t id) const
{
util::integer_to_buffer buffer{id};
return exec_prepared(stmt, buffer.c_str());
}
std::string tablespace_clause(std::string const &name)
{
std::string sql;

View File

@ -18,6 +18,7 @@
* Helper classes and functions for PostgreSQL access.
*/
#include "format.hpp"
#include "osmtypes.hpp"
#include <libpq-fe.h>
@ -104,6 +105,47 @@ private:
std::unique_ptr<PGresult, pg_result_deleter_t> m_result;
};
namespace {
/**
* Helper for pg_conn_t::exec_prepared() function. All parameters to
* that function are given to the exec_arg::to_str function which will
* pass through string-like parameters and convert other parameters to
* strings.
*/
template <typename T>
struct exec_arg
{
constexpr static std::size_t const buffers_needed = 1;
static char const *to_str(std::vector<std::string> *data, T param)
{
return data->emplace_back("{}"_format(std::forward<T>(param))).c_str();
}
};
template <>
struct exec_arg<char const *>
{
constexpr static std::size_t const buffers_needed = 0;
static char const *to_str(std::vector<std::string> * /*data*/,
char const *param)
{
return param;
}
};
template <>
struct exec_arg<std::string const &>
{
constexpr static std::size_t const buffers_needed = 0;
static char const *to_str(std::vector<std::string> * /*data*/,
std::string const &param)
{
return param.c_str();
}
};
} // anonymous namespace
/**
* PostgreSQL connection.
*
@ -117,26 +159,58 @@ class pg_conn_t
public:
explicit pg_conn_t(std::string const &conninfo);
/// Execute a prepared statement with one parameter.
pg_result_t exec_prepared(char const *stmt, char const *param) const;
/**
* Run the named prepared SQL statement and return the results.
*
* \param stmt The name of the prepared statement.
* \param params Any number of arguments (will be converted to strings
* if necessary).
* \throws exception if the command failed.
*/
template <typename... TArgs>
pg_result_t exec_prepared(char const *stmt, TArgs... params) const
{
// We have to convert all non-string parameters into strings and
// store them somewhere. We use the exec_params vector for this.
// It needs to be large enough to hold all parameters without resizing
// so that pointers into the strings in that vector remain valid
// after new parameters have been added.
constexpr auto const total_buffers_needed =
(0 + ... + exec_arg<TArgs>::buffers_needed);
std::vector<std::string> exec_params;
exec_params.reserve(total_buffers_needed);
/// Execute a prepared statement with two parameters.
pg_result_t exec_prepared(char const *stmt, char const *p1, char const *p2) const;
// This array holds the pointers to all parameter strings, either
// to the original string parameters or to the recently converted
// in the exec_params vector.
std::array<char const *, sizeof...(params)> param_ptrs = {
exec_arg<TArgs>::to_str(&exec_params,
std::forward<TArgs>(params))...};
/// Execute a prepared statement with one string parameter.
pg_result_t exec_prepared(char const *stmt, std::string const &param) const;
/// Execute a prepared statement with one integer parameter.
pg_result_t exec_prepared(char const *stmt, osmid_t id) const;
return exec_prepared_internal(stmt, sizeof...(params),
param_ptrs.data());
}
/**
* Run the specified SQL query and return the results.
*
* \param expect The expected status code of the SQL command.
* \param sql The SQL command.
* \throws exception if the result is not as expected.
*/
pg_result_t query(ExecStatusType expect, char const *sql) const;
pg_result_t query(ExecStatusType expect, std::string const &sql) const;
void set_config(char const *setting, char const *value) const;
/**
* Run the specified SQL query. This can only be used for commands that
* have no output and return status code PGRES_COMMAND_OK.
*
* \param sql The SQL command.
* \throws exception if the command failed.
*/
void exec(char const *sql) const;
void exec(std::string const &sql) const;
void copy_data(std::string const &sql, std::string const &context) const;

View File

@ -40,3 +40,73 @@ TEST_CASE("PostGIS version")
auto const postgis_version = get_postgis_version(conn);
REQUIRE(postgis_version.major >= 2);
}
TEST_CASE("query with SELECT should work")
{
auto conn = db.db().connect();
auto const result = conn.query(PGRES_TUPLES_OK, "SELECT 42");
REQUIRE(result.status() == PGRES_TUPLES_OK);
REQUIRE(result.num_fields() == 1);
REQUIRE(result.num_tuples() == 1);
REQUIRE(result.get_value_as_string(0, 0) == "42");
}
TEST_CASE("query with invalid SQL should fail")
{
auto conn = db.db().connect();
REQUIRE_THROWS(conn.query(PGRES_TUPLES_OK, "NOT-VALID-SQL"));
}
TEST_CASE("exec with invalid SQL should fail")
{
auto conn = db.db().connect();
REQUIRE_THROWS(conn.exec("XYZ"));
}
TEST_CASE("exec_prepared without parameters should work")
{
auto conn = db.db().connect();
conn.exec("PREPARE test AS SELECT 42");
auto const result = conn.exec_prepared("test");
REQUIRE(result.status() == PGRES_TUPLES_OK);
REQUIRE(result.num_fields() == 1);
REQUIRE(result.num_tuples() == 1);
REQUIRE(result.get_value_as_string(0, 0) == "42");
}
TEST_CASE("exec_prepared with single string parameters should work")
{
auto conn = db.db().connect();
conn.exec("PREPARE test(int) AS SELECT $1");
auto const result = conn.exec_prepared("test", "17");
REQUIRE(result.status() == PGRES_TUPLES_OK);
REQUIRE(result.num_fields() == 1);
REQUIRE(result.num_tuples() == 1);
REQUIRE(result.get_value_as_string(0, 0) == "17");
}
TEST_CASE("exec_prepared with string parameters should work")
{
auto conn = db.db().connect();
conn.exec("PREPARE test(int, int, int) AS SELECT $1 + $2 + $3");
auto const result = conn.exec_prepared("test", "1", "2", std::string{"3"});
REQUIRE(result.status() == PGRES_TUPLES_OK);
REQUIRE(result.num_fields() == 1);
REQUIRE(result.num_tuples() == 1);
REQUIRE(result.get_value_as_string(0, 0) == "6");
}
TEST_CASE("exec_prepared with non-string parameters should work")
{
auto conn = db.db().connect();
conn.exec("PREPARE test(int, int, int) AS SELECT $1 + $2 + $3");
auto const result = conn.exec_prepared("test", 1, 2.0, 3ULL);
REQUIRE(result.status() == PGRES_TUPLES_OK);
REQUIRE(result.num_fields() == 1);
REQUIRE(result.num_tuples() == 1);
REQUIRE(result.get_value_as_string(0, 0) == "6");
}