Merge branch 'conn-params-improve'

Improvements to connection string handling: don't duplicate parsing code
in Firebird, Oracle, PostgreSQL and SQLite backends.

Also allow using connection_parameters::set_option() to set some option
instead of having to specify it in the connection string itself.

See #1176.
This commit is contained in:
Vadim Zeitlin
2024-11-10 16:12:08 +01:00
19 changed files with 651 additions and 523 deletions
-5
View File
@@ -62,11 +62,6 @@ Note that in the single-row operation:
* bulk queries are not supported, and
* in order to fulfill the expectations of the underlying client library, the complete rowset has to be exhausted before executing further queries on the same session.
Also please note that single rows mode requires PostgreSQL 9 or later, both at
compile- and run-time. If you need to support earlier versions of PostgreSQL,
you can define `SOCI_POSTGRESQL_NOSINGLEROWMODE` when building the library to
disable it.
Once you have created a `session` object as shown above, you can use it to access the database, for example:
```cpp
+10 -3
View File
@@ -47,14 +47,21 @@ session sql("sqlite3", "db=db.sqlite timeout=2 shared_cache=true");
The set of parameters used in the connection string for SQLite is:
* `dbname` or `db`
* `dbname` or `db` - this parameter is required unless the entire connection
string is just the database name, in which case it must not contain any `=`
signs.
* `timeout` - set sqlite busy timeout (in seconds) ([link](http://www.sqlite.org/c3ref/busy_timeout.html))
* `readonly` - open database in read-only mode instead of the default read-write (note that the database file must already exist in this case, see [the documentation](https://www.sqlite.org/c3ref/open.html))
* `nocreate` - open an existing database without creating a new one if it doesn't already exist (by default, a new database file is created).
* `synchronous` - set the pragma synchronous flag ([link](http://www.sqlite.org/pragma.html#pragma_synchronous))
* `shared_cache` - should be `true` ([link](http://www.sqlite.org/c3ref/enable_shared_cache.html))
* `shared_cache` - enable or disabled shared pager cache ([link](http://www.sqlite.org/c3ref/enable_shared_cache.html))
* `vfs` - set the SQLite VFS used to as OS interface. The VFS should be registered before opening the connection, see [the documenation](https://www.sqlite.org/vfs.html)
* `foreign_keys` - set the pragma foreign_keys flag ([link](https://www.sqlite.org/pragma.html#pragma_foreign_keys)).
* `foreign_keys` - set the pragma `foreign_keys` flag ([link](https://www.sqlite.org/pragma.html#pragma_foreign_keys)).
Boolean options `readonly`, `nocreate`, and `shared_cache` can be either
specified without any value, which is equivalent to setting them to `1`, or set
to one of `1`, `yes`, `true` or `on` to enable the option or `0`, `no`, `false`
or `off` to disable it. Specifying any other value results in an error.
Once you have created a `session` object as shown above, you can use it to access the database, for example:
+58
View File
@@ -0,0 +1,58 @@
//
// Copyright (C) 2024 Vadim Zeitlin.
// 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 SOCI_PRIVATE_SOCI_CASE_H_INCLUDED
#define SOCI_PRIVATE_SOCI_CASE_H_INCLUDED
#include <cctype>
#include <string>
namespace soci
{
namespace details
{
// Simplistic conversions of strings to upper/lower case.
//
// This doesn't work correctly for arbitrary Unicode strings for well-known
// reasons (such conversions can't be done correctly on char by char basis),
// but they do work for ASCII strings that we deal with and for anything else
// we'd need ICU -- which we could start using later, if necessary, by just
// replacing these functions with the versions using ICU functions instead.
inline std::string string_toupper(std::string const& s)
{
std::string res;
res.reserve(s.size());
for (char c : s)
{
res += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return res;
}
inline std::string string_tolower(std::string const& s)
{
std::string res;
res.reserve(s.size());
for (char c : s)
{
res += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return res;
}
} // namespace details
} // namespace soci
#endif // SOCI_PRIVATE_SOCI_CASE_H_INCLUDED
+42 -3
View File
@@ -51,6 +51,22 @@ public:
void set_connect_string(const std::string & connectString) { connectString_ = connectString; }
std::string const & get_connect_string() const { return connectString_; }
// For some (but not all) backends the connection string consists of
// space-separated name=value pairs. This function parses the string
// assuming it uses this syntax and sets the options accordingly.
//
// If it detects invalid syntax, e.g. a name without a corresponding value,
// it throws an exception.
//
// Note that currently unknown options are simply ignored.
void extract_options_from_space_separated_string();
// Build a space-separated string from the options, quoting the options
// values using the provided quote character.
std::string build_string_from_options(char quote) const;
// Set the value of the given option, overwriting any previous value.
void set_option(const char * name, std::string const & value)
{
@@ -70,12 +86,35 @@ public:
return true;
}
// Return true if the option with the given name was found with option_true
// value.
// Same as get_option(), but also removes the option from the connection
// string if it was present in it.
bool extract_option(const char * name, std::string & value)
{
Options::iterator const it = options_.find(name);
if (it == options_.end())
return false;
value = it->second;
options_.erase(it);
return true;
}
// Return true if the option with the given name has one of the values
// considered to be true, i.e. "1", "yes", "true" or "on" or is empty.
// Return false if the value is one of "0", "no", "false" or "off" or the
// option was not specified at all.
//
// Throw an exception if the option was given but the value is none of
// the above, comparing case-insensitively.
static bool is_true_value(const char * name, std::string const & value);
// Return true if the option with the given name was found with a "true"
// value in the sense of is_true_value() above.
bool is_option_on(const char * name) const
{
std::string value;
return get_option(name, value) && value == option_true;
return get_option(name, value) && is_true_value(name, value);
}
private:
+1 -2
View File
@@ -384,8 +384,7 @@ private:
struct SOCI_POSTGRESQL_DECL postgresql_session_backend : details::session_backend
{
postgresql_session_backend(connection_parameters const & parameters,
bool single_row_mode);
explicit postgresql_session_backend(connection_parameters const & parameters);
~postgresql_session_backend() override;
+8 -186
View File
@@ -9,7 +9,6 @@
#include "soci/firebird/soci-firebird.h"
#include "firebird/error-firebird.h"
#include "soci/session.h"
#include <locale>
#include <map>
#include <sstream>
#include <string>
@@ -20,182 +19,6 @@ using namespace soci::details::firebird;
namespace
{
// Helpers of explodeISCConnectString() for reading words from a string. "Word"
// here is defined very loosely as just a sequence of non-space characters.
//
// All these helper functions update the input iterator to point to the first
// character not consumed by them.
// Advance the input iterator until the first non-space character or end of the
// string.
void skipWhiteSpace(std::string::const_iterator& i, std::string::const_iterator const &end)
{
std::locale const loc;
for (; i != end; ++i)
{
if (!std::isspace(*i, loc))
break;
}
}
// Return the string of all characters until the first space or the specified
// delimiter.
//
// Throws if the first non-space character after the end of the word is not the
// delimiter. However just returns en empty string, without throwing, if
// nothing is left at all in the string except for white space.
std::string
getWordUntil(std::string const &s, std::string::const_iterator &i, char delim)
{
std::string::const_iterator const end = s.end();
skipWhiteSpace(i, end);
// We need to handle this case specially because it's not an error if
// nothing at all remains in the string. But if anything does remain, then
// we must have the delimiter.
if (i == end)
return std::string();
// Simply put anything until the delimiter into the word, stopping at the
// first white space character.
std::string word;
std::locale const loc;
for (; i != end; ++i)
{
if (*i == delim)
break;
if (std::isspace(*i, loc))
{
skipWhiteSpace(i, end);
if (i == end || *i != delim)
{
std::ostringstream os;
os << "Expected '" << delim << "' at position "
<< (i - s.begin() + 1)
<< " in Firebird connection string \""
<< s << "\".";
throw soci_error(os.str());
}
break;
}
word += *i;
}
if (i == end)
{
std::ostringstream os;
os << "Expected '" << delim
<< "' not found before the end of the string "
<< "in Firebird connection string \""
<< s << "\".";
throw soci_error(os.str());
}
++i; // Skip the delimiter itself.
return word;
}
// Return a possibly quoted word, i.e. either just a sequence of non-space
// characters or everything inside a double-quoted string.
//
// Throws if the word is quoted and the closing quote is not found. However
// doesn't throw, just returns an empty string if there is nothing left.
std::string
getPossiblyQuotedWord(std::string const &s, std::string::const_iterator &i)
{
std::string::const_iterator const end = s.end();
skipWhiteSpace(i, end);
std::string word;
if (i != end && *i == '"')
{
for (;;)
{
if (++i == end)
{
std::ostringstream os;
os << "Expected '\"' not found before the end of the string "
"in Firebird connection string \""
<< s << "\".";
throw soci_error(os.str());
}
if (*i == '"')
{
++i;
break;
}
word += *i;
}
}
else // Not quoted.
{
std::locale const loc;
for (; i != end; ++i)
{
if (std::isspace(*i, loc))
break;
word += *i;
}
}
return word;
}
// retrieves parameters from the uniform connect string which is supposed to be
// in the form "key=value[ key2=value2 ...]" and the values may be quoted to
// allow including spaces into them. Notice that currently there is no way to
// include both a space and a double quote in a value.
std::map<std::string, std::string>
explodeISCConnectString(std::string const &connectString)
{
std::map<std::string, std::string> parameters;
std::string key, value;
for (std::string::const_iterator i = connectString.begin(); ; )
{
key = getWordUntil(connectString, i, '=');
if (key.empty())
break;
value = getPossiblyQuotedWord(connectString, i);
parameters.insert(std::pair<std::string, std::string>(key, value));
}
return parameters;
}
// extracts given parameter from map previusly build with explodeISCConnectString
bool getISCConnectParameter(std::map<std::string, std::string> const & m, std::string const & key,
std::string & value)
{
std::map <std::string, std::string> :: const_iterator i;
value.clear();
i = m.find(key);
if (i != m.end())
{
value = i->second;
return true;
}
else
{
return false;
}
}
void setDPBOption(std::string& dpb, int const option, std::string const & value)
{
@@ -216,36 +39,35 @@ firebird_session_backend::firebird_session_backend(
connection_parameters const & parameters) : dbhp_(0), trhp_(0)
, decimals_as_strings_(false)
{
// extract connection parameters
std::map<std::string, std::string>
params(explodeISCConnectString(parameters.get_connect_string()));
auto params = parameters;
params.extract_options_from_space_separated_string();
ISC_STATUS stat[stat_size];
std::string param;
// preparing connection options
std::string dpb;
if (getISCConnectParameter(params, "user", param))
if (params.get_option("user", param))
{
setDPBOption(dpb, isc_dpb_user_name, param);
}
if (getISCConnectParameter(params, "password", param))
if (params.get_option("password", param))
{
setDPBOption(dpb, isc_dpb_password, param);
}
if (getISCConnectParameter(params, "role", param))
if (params.get_option("role", param))
{
setDPBOption(dpb, isc_dpb_sql_role_name, param);
}
if (getISCConnectParameter(params, "charset", param))
if (params.get_option("charset", param))
{
setDPBOption(dpb, isc_dpb_lc_ctype, param);
}
if (getISCConnectParameter(params, "service", param) == false)
if (!params.get_option("service", param))
{
throw soci_error("Service name not specified.");
}
@@ -258,7 +80,7 @@ firebird_session_backend::firebird_session_backend(
throw_iscerror(stat);
}
if (getISCConnectParameter(params, "decimals_as_strings", param))
if (params.get_option("decimals_as_strings", param))
{
decimals_as_strings_ = param == "1" || param == "Y" || param == "y";
}
+51 -127
View File
@@ -9,11 +9,13 @@
#include "soci/oracle/soci-oracle.h"
#include "soci/connection-parameters.h"
#include "soci/backend-loader.h"
#include "soci-cstrtoi.h"
#include <cctype>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <sstream>
#ifdef _MSC_VER
#pragma warning(disable:4355)
@@ -22,53 +24,6 @@
using namespace soci;
using namespace soci::details;
// iterates the string pointed by i, searching for pairs of key value.
// it returns the position after the value
std::string::const_iterator get_key_value(std::string::const_iterator & i,
std::string::const_iterator const & end,
std::string & key,
std::string & value)
{
bool in_value = false;
bool quoted = false;
key.clear();
value.clear();
while (i != end)
{
if (in_value == false)
{
if (*i == '=')
{
in_value = true;
if (i != end && *(i + 1) == '"')
{
quoted = true;
++i; // jump over the quote
}
}
else if (!isspace(*i))
{
key += *i;
}
}
else
{
if ((quoted == true && *i == '"') || (quoted == false && isspace(*i)))
{
return ++i;
}
else
{
value += *i;
}
}
++i;
}
return i;
}
// decode charset and ncharset names
int charset_code(const std::string & name)
{
@@ -92,11 +47,7 @@ int charset_code(const std::string & name)
else
{
// allow explicit number value
std::istringstream ss(name);
ss >> code;
if (!ss)
if (!cstring_to_unsigned(code, name.c_str()))
{
throw soci_error("Invalid character set name.");
}
@@ -105,84 +56,57 @@ int charset_code(const std::string & name)
return code;
}
// retrieves service name, user name and password from the
// uniform connect string
void chop_connect_string(std::string const & connectString,
std::string & serviceName, std::string & userName,
std::string & password, int & mode, bool & decimals_as_strings,
int & charset, int & ncharset)
{
serviceName.clear();
userName.clear();
password.clear();
mode = OCI_DEFAULT;
decimals_as_strings = false;
charset = 0;
ncharset = 0;
std::string key, value;
std::string::const_iterator i = connectString.begin();
while (i != connectString.end())
{
i = get_key_value(i, connectString.end(), key, value);
if (key == "service")
{
serviceName = value;
}
else if (key == "user")
{
userName = value;
}
else if (key == "password")
{
password = value;
}
else if (key == "mode")
{
if (value == "sysdba")
{
mode = OCI_SYSDBA;
}
else if (value == "sysoper")
{
mode = OCI_SYSOPER;
}
else if (value == "default")
{
mode = OCI_DEFAULT;
}
else
{
throw soci_error("Invalid connection mode.");
}
}
else if (key == "decimals_as_strings")
{
decimals_as_strings = value == "1" || value == "Y" || value == "y";
}
else if (key == "charset")
{
charset = charset_code(value);
}
else if (key == "ncharset")
{
ncharset = charset_code(value);
}
}
}
// concrete factory for Empty concrete strategies
oracle_session_backend * oracle_backend_factory::make_session(
connection_parameters const & parameters) const
{
std::string serviceName, userName, password;
int mode;
bool decimals_as_strings;
int charset;
int ncharset;
std::string value;
chop_connect_string(parameters.get_connect_string(), serviceName, userName, password,
mode, decimals_as_strings, charset, ncharset);
auto params = parameters;
params.extract_options_from_space_separated_string();
std::string serviceName, userName, password;
params.get_option("service", serviceName);
params.get_option("user", userName);
params.get_option("password", password);
int mode = OCI_DEFAULT;
if (params.get_option("mode", value))
{
if (value == "sysdba")
{
mode = OCI_SYSDBA;
}
else if (value == "sysoper")
{
mode = OCI_SYSOPER;
}
else if (value == "default")
{
mode = OCI_DEFAULT;
}
else
{
throw soci_error("Invalid connection mode.");
}
}
bool decimals_as_strings = false;
if (params.get_option("decimals_as_strings", value))
{
decimals_as_strings = value == "1" || value == "Y" || value == "y";
}
int charset = 0;
if (params.get_option("charset", value))
{
charset = charset_code(value);
}
int ncharset = 0;
if (params.get_option("ncharset", value))
{
ncharset = charset_code(value);
}
return new oracle_session_backend(serviceName, userName, password,
mode, decimals_as_strings, charset, ncharset);
+1 -94
View File
@@ -18,103 +18,10 @@
using namespace soci;
using namespace soci::details;
namespace // unnamed
{
// iterates the string pointed by i, searching for pairs of key value.
// it returns the position after the value
std::string::const_iterator get_key_value(std::string::const_iterator & i,
std::string::const_iterator const & end,
std::string & key,
std::string & value)
{
bool in_value = false;
bool quoted = false;
key.clear();
value.clear();
while (i != end)
{
if (in_value == false)
{
if (*i == '=')
{
in_value = true;
if (i != end && *(i + 1) == '"')
{
quoted = true;
++i; // jump over the quote
}
}
else if (!isspace(*i))
{
key += *i;
}
}
else
{
if ((quoted == true && *i == '"') || (quoted == false && isspace(*i)))
{
return ++i;
}
else
{
value += *i;
}
}
++i;
}
return i;
}
// retrieves specific parameters from the
// uniform connect string
std::string chop_connect_string(std::string const & connectString,
bool & single_row_mode)
{
std::string pruned_conn_string;
single_row_mode = false;
std::string key, value;
std::string::const_iterator i = connectString.begin();
while (i != connectString.end())
{
i = get_key_value(i, connectString.end(), key, value);
if (key == "singlerow" || key == "singlerows")
{
single_row_mode = (value == "true" || value == "yes");
}
else
{
if (pruned_conn_string.empty() == false)
{
pruned_conn_string += ' ';
}
pruned_conn_string += key + '=' + value;
}
}
return pruned_conn_string;
}
} // unnamed namespace
// concrete factory for Empty concrete strategies
postgresql_session_backend * postgresql_backend_factory::make_session(
connection_parameters const & parameters) const
{
bool single_row_mode;
const std::string pruned_conn_string =
chop_connect_string(parameters.get_connect_string(), single_row_mode);
connection_parameters pruned_parameters(parameters);
pruned_parameters.set_connect_string(pruned_conn_string);
return new postgresql_session_backend(pruned_parameters, single_row_mode);
return new postgresql_session_backend(parameters);
}
postgresql_backend_factory const soci::postgresql;
+30 -3
View File
@@ -141,10 +141,10 @@ std::string create_case_list_of_strings(const std::vector<std::string>& list)
} // namespace unnamed
postgresql_session_backend::postgresql_session_backend(
connection_parameters const& parameters, bool single_row_mode)
connection_parameters const& parameters)
: statementCount_(0), conn_(0)
{
single_row_mode_ = single_row_mode;
single_row_mode_ = false;
connect(parameters);
}
@@ -152,7 +152,34 @@ postgresql_session_backend::postgresql_session_backend(
void postgresql_session_backend::connect(
connection_parameters const& parameters)
{
PGconn* conn = PQconnectdb(parameters.get_connect_string().c_str());
auto params = parameters;
params.extract_options_from_space_separated_string();
// Extract SOCI-specific options, i.e. check if they're present and remove
// them from params to avoid passing them to PQconnectdb() below.
std::string value;
// This one is not used by this backend, but can be present in the
// connection string if we're called from session::reconnect().
params.extract_option(option_reconnect, value);
// Notice that we accept both variants only for compatibility.
char const* name;
if (params.extract_option("singlerow", value))
name = "singlerow";
else if (params.extract_option("singlerows", value))
name = "singlerows";
else
name = nullptr;
if (name)
{
single_row_mode_ = connection_parameters::is_true_value(name, value);
}
// We can't use SOCI connection string with PQconnectdb() directly because
// libpq uses single quotes instead of double quotes used by SOCI.
PGconn* conn = PQconnectdb(params.build_string_from_options('\'').c_str());
if (0 == conn || CONNECTION_OK != PQstatus(conn))
{
std::string msg = "Cannot establish connection to the database.";
-30
View File
@@ -23,7 +23,6 @@ namespace // unnamed
{
// used only with asynchronous operations in single-row mode
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
void wait_until_operation_complete(postgresql_session_backend & session)
{
for (;;)
@@ -49,7 +48,6 @@ void throw_soci_error(PGconn * conn, const char * msg)
throw soci_error(description);
}
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
} // unnamed namespace
@@ -61,12 +59,6 @@ postgresql_statement_backend::postgresql_statement_backend(
hasIntoElements_(false), hasVectorIntoElements_(false),
hasUseElements_(false), hasVectorUseElements_(false)
{
#ifdef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode)
{
throw soci_error("Single row mode not supported in this version of the library");
}
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
}
postgresql_statement_backend::~postgresql_statement_backend()
@@ -229,7 +221,6 @@ void postgresql_statement_backend::prepare(std::string const & query,
// if it fails to prepare it we can't DEALLOCATE it.
std::string statementName = session_.get_next_statement_name();
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
// prepare for single-row retrieval
@@ -245,7 +236,6 @@ void postgresql_statement_backend::prepare(std::string const & query,
wait_until_operation_complete(session_);
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row query execution
@@ -265,12 +255,10 @@ void postgresql_statement_backend::prepare(std::string const & query,
statement_backend::exec_fetch_result
postgresql_statement_backend::execute(int number)
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_ && (number > 1))
{
throw soci_error("Bulk operations are not supported with single-row mode.");
}
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
// If the statement was "just described", then we know that
// it was actually executed with all the use elements
@@ -365,7 +353,6 @@ postgresql_statement_backend::execute(int number)
{
// this query was separately prepared
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
int result = PQsendQueryPrepared(session_.conn_,
@@ -386,7 +373,6 @@ postgresql_statement_backend::execute(int number)
}
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -401,7 +387,6 @@ postgresql_statement_backend::execute(int number)
// this query was not separately prepared and should
// be executed as a one-time query
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
int result = PQsendQueryParams(session_.conn_, query_.c_str(),
@@ -421,7 +406,6 @@ postgresql_statement_backend::execute(int number)
}
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -462,7 +446,6 @@ postgresql_statement_backend::execute(int number)
{
// this query was separately prepared
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
int result = PQsendQueryPrepared(session_.conn_,
@@ -481,7 +464,6 @@ postgresql_statement_backend::execute(int number)
}
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -491,7 +473,6 @@ postgresql_statement_backend::execute(int number)
}
else // stType_ == st_one_time_query
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
int result = PQsendQuery(session_.conn_, query_.c_str());
@@ -509,7 +490,6 @@ postgresql_statement_backend::execute(int number)
}
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -520,7 +500,6 @@ postgresql_statement_backend::execute(int number)
}
bool process_result;
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
if (justDescribed_)
@@ -537,7 +516,6 @@ postgresql_statement_backend::execute(int number)
process_result = result_.check_for_data("Cannot execute query.");
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -579,12 +557,10 @@ postgresql_statement_backend::execute(int number)
statement_backend::exec_fetch_result
postgresql_statement_backend::fetch(int number)
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_ && (number > 1))
{
throw soci_error("Bulk operations are not supported with single-row mode.");
}
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
// Note:
// In the multi-row mode this function does not actually fetch anything from anywhere
@@ -599,7 +575,6 @@ postgresql_statement_backend::fetch(int number)
if (currentRow_ >= numberOfRows_)
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
PGresult* res = PQgetResult(session_.conn_);
@@ -626,7 +601,6 @@ postgresql_statement_backend::fetch(int number)
}
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -639,7 +613,6 @@ postgresql_statement_backend::fetch(int number)
{
if (currentRow_ + number > numberOfRows_)
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
rowsToConsume_ = 1;
@@ -647,7 +620,6 @@ postgresql_statement_backend::fetch(int number)
return ef_success;
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
// default multi-row execution
@@ -661,13 +633,11 @@ postgresql_statement_backend::fetch(int number)
}
else
{
#ifndef SOCI_POSTGRESQL_NOSINGLEROWMODE
if (single_row_mode_)
{
rowsToConsume_ = 1;
}
else
#endif // !SOCI_POSTGRESQL_NOSINGLEROWMODE
{
rowsToConsume_ = number;
}
+42 -57
View File
@@ -115,66 +115,51 @@ sqlite3_session_backend::sqlite3_session_backend(
std::string synchronous;
std::string foreignKeys;
std::string const & connectString = parameters.get_connect_string();
std::string dbname(connectString);
std::stringstream ssconn(connectString);
while (!ssconn.eof() && ssconn.str().find('=') != std::string::npos)
std::string dbname;
auto params = parameters;
if (connectString.find('=') == std::string::npos)
{
std::string key, val;
std::getline(ssconn, key, '=');
std::getline(ssconn, val, ' ');
// The entire connection string must be just the database name.
dbname = connectString;
}
else
{
params.extract_options_from_space_separated_string();
}
if (val.size()>0 && val[0]=='\"')
{
std::string quotedVal = val.erase(0, 1);
std::string val;
if (params.get_option("dbname", val) || params.get_option("db", val))
{
dbname = val;
}
if (params.get_option("timeout", val))
{
std::istringstream converter(val);
converter >> timeout;
}
if (params.get_option("synchronous", val))
{
synchronous = val;
}
if (params.is_option_on("readonly"))
{
connection_flags = (connection_flags | SQLITE_OPEN_READONLY) & ~(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
}
if (params.is_option_on("nocreate"))
{
connection_flags &= ~SQLITE_OPEN_CREATE;
}
if (params.is_option_on("shared_cache"))
{
connection_flags |= SQLITE_OPEN_SHAREDCACHE;
}
params.get_option("vfs", vfs);
params.get_option("foreign_keys", foreignKeys);
if (quotedVal[quotedVal.size()-1] == '\"')
{
quotedVal.erase(val.size()-1);
}
else // space inside value string
{
std::getline(ssconn, val, '\"');
quotedVal = quotedVal + " " + val;
std::string keepspace;
std::getline(ssconn, keepspace, ' ');
}
val = quotedVal;
}
if ("dbname" == key || "db" == key)
{
dbname = val;
}
else if ("timeout" == key)
{
std::istringstream converter(val);
converter >> timeout;
}
else if ("synchronous" == key)
{
synchronous = val;
}
else if ("readonly" == key)
{
connection_flags = (connection_flags | SQLITE_OPEN_READONLY) & ~(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
}
else if ("nocreate" == key)
{
connection_flags &= ~SQLITE_OPEN_CREATE;
}
else if ("shared_cache" == key && "true" == val)
{
connection_flags |= SQLITE_OPEN_SHAREDCACHE;
}
else if ("vfs" == key)
{
vfs = val;
}
else if ("foreign_keys" == key)
{
foreignKeys = val;
}
if (dbname.empty())
{
throw sqlite3_soci_error("Database name must be specified", 0);
}
int res = sqlite3_open_v2(dbname.c_str(), &conn_, connection_flags, (vfs.empty()?NULL:vfs.c_str()));
+3 -7
View File
@@ -16,16 +16,12 @@
#include <sstream>
#include <string>
#include "soci-case.h"
#ifdef _MSC_VER
#pragma warning(disable:4355)
#endif
// This is used instead of tolower() just to avoid warnings about int to char
// casts inside MSVS std::transform() implementation.
char toLowerCh(char c) {
return static_cast<char>( std::tolower(c) );
}
using namespace soci;
using namespace soci::details;
using namespace sqlite_api;
@@ -550,7 +546,7 @@ void sqlite3_statement_backend::describe_column(int colNum,
dt.resize(siter - dt.begin());
// do all comparisons in lower case
std::transform(dt.begin(), dt.end(), dt.begin(), toLowerCh);
dt = string_tolower(dt);
sqlite3_data_type_map::const_iterator iter = dataTypeMap.find(dt);
if (iter != dataTypeMap.end())
+209
View File
@@ -10,6 +10,8 @@
#include "soci/soci-backend.h"
#include "soci/backend-loader.h"
#include "soci-case.h"
char const * soci::option_reconnect = "reconnect";
char const * soci::option_true = "1";
@@ -171,4 +173,211 @@ void connection_parameters::reset_after_move()
backendRef_ = nullptr;
}
/* static */
bool
connection_parameters::is_true_value(char const* name, std::string const& value)
{
// At least for compatibility (but also because this is convenient and
// makes sense), we accept "readonly" as synonym for "readonly=1" etc.
if (value.empty())
return true;
std::string const val = details::string_tolower(value);
if (val == "1" || val == "yes" || val == "true" || val == "on")
return true;
if (val == "0" || val == "no" || val == "false" || val == "off")
return false;
std::ostringstream os;
os << R"(Invalid value ")"
<< value
<< R"(" for boolean option ")"
<< name
<< '"';
throw soci_error(os.str());
}
namespace
{
// Helpers of extract_options_from_space_separated_string() for reading words
// from a string. "Word" here is defined very loosely as just a sequence of
// non-space characters.
// We could use std::isspace() but it doesn't seem worth it to create a locale
// just for this.
inline bool isSpace(std::string::const_iterator i)
{
return *i == ' ' || *i == '\t';
}
// All the functions below update the input iterator to point to the first
// character not consumed by them.
// Advance the input iterator until the first non-space character or end of the
// string.
void
skipWhiteSpace(std::string::const_iterator& i,
std::string::const_iterator const& end)
{
for (; i != end; ++i)
{
if (!isSpace(i))
break;
}
}
// Return a possibly quoted word, i.e. either just a sequence of non-space
// characters or everything inside a double-quoted string.
//
// Throws if the word is quoted and the closing quote is not found. However
// doesn't throw, just returns an empty string if there is nothing left.
std::string
getPossiblyQuotedWord(std::string const &s, std::string::const_iterator &i)
{
std::string::const_iterator const end = s.end();
skipWhiteSpace(i, end);
std::string word;
if (i != end && *i == '"')
{
for (;;)
{
if (++i == end)
{
std::ostringstream os;
os << "Expected '\"' not found before the end of the string "
"in the connection string \""
<< s << "\".";
throw soci_error(os.str());
}
if (*i == '"')
{
++i;
break;
}
word += *i;
}
}
else // Not quoted.
{
for (; i != end; ++i)
{
if (isSpace(i))
break;
word += *i;
}
}
return word;
}
} // namespace anonymous
void connection_parameters::extract_options_from_space_separated_string()
{
constexpr char delim = '=';
std::string::const_iterator const end = connectString_.end();
for (std::string::const_iterator i = connectString_.begin(); ; )
{
skipWhiteSpace(i, end);
// Anything until the delimiter or space is the name.
std::string name;
std::string value;
for (;;)
{
if (i == end || isSpace(i))
break;
if (*i == delim)
{
if (name.empty())
{
std::ostringstream os;
os << "Unexpected '"
<< delim
<< "' without a name at position "
<< (i - connectString_.begin() + 1)
<< " in the connection string \""
<< connectString_
<< "\".";
throw soci_error(os.str());
}
++i; // Skip the delimiter itself.
// And get the option value which follows it.
value = getPossiblyQuotedWord(connectString_, i);
break;
}
name += *i++;
}
if (name.empty())
{
// We've reached the end of the string and there is nothing left.
break;
}
// Note that value may be empty here, we intentionally allow specifying
// options without values, e.g. just "switch" instead of "switch=1".
options_[name] = value;
}
}
std::string
connection_parameters::build_string_from_options(char quote) const
{
std::string res;
for (auto const& option : options_)
{
if (!res.empty())
{
res += ' ';
}
res += option.first;
res += '=';
// Quote the value if it contains spaces or the quote character itself.
auto const& value = option.second;
if (value.empty() ||
value.find(' ') != std::string::npos ||
value.find(quote) != std::string::npos)
{
res += quote;
for (char c : value)
{
if (c == quote)
{
res += '\\';
}
res += c;
}
res += quote;
}
else
{
res += value;
}
}
return res;
}
} // namespace soci
+3 -5
View File
@@ -10,10 +10,11 @@
#include "soci/type-holder.h"
#include <cstddef>
#include <cctype>
#include <sstream>
#include <string>
#include "soci-case.h"
using namespace soci;
using namespace details;
@@ -40,10 +41,7 @@ void row::add_properties(column_properties const &cp)
std::string const & originalName = cp.get_name();
if (uppercaseColumnNames_)
{
for (std::size_t i = 0; i != originalName.size(); ++i)
{
columnName.push_back(static_cast<char>(std::toupper(originalName[i])));
}
columnName = string_toupper(originalName);
// rewrite the column name in the column_properties object
// as well to retain consistent views
+1
View File
@@ -28,6 +28,7 @@ add_library(soci_tests_common STATIC
common/test-main.cpp
common/test-boost.cpp
common/test-common.cpp
common/test-connparams.cpp
common/test-custom.cpp
common/test-dynamic.cpp
common/test-lob.cpp
+2
View File
@@ -3730,6 +3730,7 @@ TEST_CASE_METHOD(common_tests, "Logger", "[core][log]")
//
// These variables are defined in other files we want to force linking with.
extern volatile bool soci_use_test_boost;
extern volatile bool soci_use_test_connparams;
extern volatile bool soci_use_test_custom;
extern volatile bool soci_use_test_dynamic;
extern volatile bool soci_use_test_lob;
@@ -3742,6 +3743,7 @@ test_context_common::test_context_common()
soci_use_test_boost = true;
#endif
soci_use_test_connparams = true;
soci_use_test_custom = true;
soci_use_test_dynamic = true;
soci_use_test_lob = true;
+139
View File
@@ -0,0 +1,139 @@
//
// Copyright (C) 2024 Vadim Zeitlin
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
//
#include "soci/soci.h"
#include <catch.hpp>
#include "test-context.h"
namespace soci
{
namespace tests
{
// This variable is referenced from test-common.cpp to force linking this file.
volatile bool soci_use_test_connparams = false;
// A helper to check that parsing the given connection string works.
connection_parameters parse_connection_string(std::string const& connstr)
{
connection_parameters params(backEnd, connstr);
REQUIRE_NOTHROW(params.extract_options_from_space_separated_string());
return params;
}
// A similar one checking that the given connection string is invalid.
void check_invalid_connection_string(std::string const& connstr)
{
INFO(R"(Parsing invalid connection string ")" << connstr << R"(")");
connection_parameters params(backEnd, connstr);
CHECK_THROWS_AS(params.extract_options_from_space_separated_string(),
soci_error);
}
// Another helper to check that the given option has the expected value.
void
check_option(connection_parameters const& params,
char const* name,
std::string const& expected)
{
std::string value;
if ( params.get_option(name, value) )
{
CHECK(value == expected);
}
else
{
FAIL_CHECK(R"(Option ")" << name << R"(" not found)");
}
}
TEST_CASE("Connection string parsing", "[core][connstring]")
{
SECTION("Invalid")
{
connection_parameters params(backEnd, "");
// Missing name.
check_invalid_connection_string("=");
check_invalid_connection_string("foo=ok =bar");
// Missing quote.
check_invalid_connection_string(R"(foo=")");
check_invalid_connection_string(R"(foo="bar)");
check_invalid_connection_string(R"(foo="bar" baz="quux )");
// This one is not invalid (empty values are allowed), but it check
// because it used to dereference an invalid iterator (see #1175).
REQUIRE_NOTHROW(parse_connection_string("bloordyblop="));
}
SECTION("Typical")
{
std::string const s = "dbname=/some/path host=some.where readonly=1 port=1234";
INFO(R"(Parsing connection string ")" << s << R"(")");
auto params = parse_connection_string(s);
check_option(params, "dbname", "/some/path");
check_option(params, "host", "some.where");
check_option(params, "port", "1234");
check_option(params, "readonly", "1");
std::string value;
CHECK_FALSE(params.get_option("user", value));
}
SECTION("Quotes")
{
std::string const s = R"(user="foo" pass="" service="bar baz")";
INFO(R"(Parsing connection string ")" << s << R"(")");
auto params = parse_connection_string(s);
check_option(params, "user", "foo");
check_option(params, "pass", "");
check_option(params, "service", "bar baz");
}
}
TEST_CASE("connection_parameters::extract_option", "[core][connstring]")
{
std::string value;
connection_parameters params(backEnd, "foo bar=baz");
params.extract_options_from_space_separated_string();
// Extracting an option must remove it.
CHECK(params.extract_option("foo", value));
CHECK(!params.get_option("foo", value));
CHECK_FALSE(params.extract_option("baz", value));
CHECK(params.extract_option("bar", value));
CHECK(!params.get_option("bar", value));
}
TEST_CASE("connection_parameters::build_string_from_options", "[core][connstring]")
{
connection_parameters params(backEnd, R"(foo bar="baz" quux="1 2")");
params.extract_options_from_space_separated_string();
// Check that unecessary quotes are removed, empty value are explicitly
// specified and the required quotes are kept.
CHECK(params.build_string_from_options('\'') == "bar=baz foo='' quux='1 2'");
}
} // namespace tests
} // namespace soci
+12 -1
View File
@@ -130,6 +130,17 @@ struct oid_table_creator : public table_creator_base
}
};
TEST_CASE("PostgreSQL connection string", "[postgresql][connstring]")
{
// There are no required parts in libpq connection string, so we can only
// test that invalid options are detected.
CHECK_THROWS_WITH(soci::session(backEnd, "bloordyblop=1"),
Catch::Contains(R"(invalid connection option "bloordyblop")"));
CHECK_THROWS_WITH(soci::session(backEnd, "sslmode=bloordyblop"),
Catch::Contains(R"(invalid sslmode value: "bloordyblop")"));
}
// ROWID test
// Note: in PostgreSQL, there is no ROWID, there is OID.
// It is still provided as a separate type for "portability",
@@ -1483,7 +1494,7 @@ public:
std::string get_example_connection_string() const override
{
return "Host=localhost;Port=5432;Database=test;User=postgres;Password=postgres";
return "host=localhost port=5432 dbname=test user=postgres password=postgres";
}
table_creator_base* table_creator_1(soci::session& s) const override
+39
View File
@@ -17,6 +17,45 @@ using namespace soci::tests;
std::string connectString;
backend_factory const &backEnd = *soci::factory_sqlite3();
TEST_CASE("SQLite connection string", "[sqlite][connstring]")
{
CHECK_THROWS_WITH(soci::session(backEnd, ""),
Catch::Contains("Database name must be specified"));
CHECK_THROWS_WITH(soci::session(backEnd, "readonly=1"),
Catch::Contains("Database name must be specified"));
CHECK_THROWS_WITH(soci::session(backEnd, "readonly=\""),
Catch::Contains("Expected '\"'"));
CHECK_THROWS_WITH(soci::session(backEnd, "readonly=maybe"),
Catch::Contains("Invalid value"));
CHECK_THROWS_WITH(soci::session(backEnd, "db=no-such-file nocreate=1"),
Catch::Contains("Cannot establish connection"));
CHECK_NOTHROW(soci::session(backEnd, "dbname=:memory: nocreate"));
CHECK_NOTHROW(soci::session(backEnd, "dbname=:memory: foreign_keys=on"));
// Also check an alternative way of specifying the connection parameters.
connection_parameters params(backEnd, "dbname=still-no-such-file");
params.set_option("foreign_keys", "1");
params.set_option("nocreate", "1");
CHECK_THROWS_WITH(soci::session(params),
Catch::Contains("Cannot establish connection"));
// Finally allow testing arbitrary connection strings by specifying them in
// the environment variables.
if (auto const connstr = std::getenv("SOCI_TEST_CONNSTR_GOOD"))
{
CHECK_NOTHROW(soci::session(backEnd, connstr));
}
if (auto const connstr = std::getenv("SOCI_TEST_CONNSTR_BAD"))
{
CHECK_THROWS_AS(soci::session(backEnd, connstr), soci_error);
}
}
// ROWID test
// In sqlite3 the row id can be called ROWID, _ROWID_ or oid
TEST_CASE("SQLite rowid", "[sqlite][rowid][oid]")