gnucash/libgnucash/backend/dbi/gnc-backend-dbi.cpp

1315 lines
39 KiB
C++

/********************************************************************
* gnc-backend-dbi.c: load and save data to SQL via libdbi *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
/** @file gnc-backend-dbi.c
* @brief load and save data to SQL
* @author Copyright (c) 2006-2008 Phil Longstaff <plongstaff@rogers.com>
*
* This file implements the top-level QofBackend API for saving/
* restoring data to/from an SQL db using libdbi
*/
extern "C"
{
#include "config.h"
#include <platform.h>
#ifdef __STRICT_ANSI__
#undef __STRICT_ANSI__
#define __STRICT_ANSI_UNSET__ 1
#endif
#ifdef _NO_OLDNAMES
#undef _NO_OLDNAMES
#endif
#ifdef _UWIN
#undef _UWIN
#endif
#if PLATFORM(WINDOWS)
#include <winsock2.h>
#include <windows.h>
#define __STDC_FORMAT_MACROS 1
#endif
#include <inttypes.h>
#include <errno.h>
#include <glib.h>
#include <glib/gstdio.h>
#include "qof.h"
#include "qofquery-p.h"
#include "qofquerycore-p.h"
#include "Account.h"
#include "TransLog.h"
#include "gnc-engine.h"
#include "SX-book.h"
#include "Recurrence.h"
#include <gnc-features.h>
#include "gnc-uri-utils.h"
#include "gnc-filepath-utils.h"
#include <gnc-path.h>
#include "gnc-locale-utils.h"
#include "gnc-prefs.h"
#ifdef S_SPLINT_S
#include "splint-defs.h"
#endif
}
#include <boost/regex.hpp>
#include <string>
#include <iomanip>
#include <qofsession.hpp>
#include <gnc-backend-prov.hpp>
#include "gnc-backend-dbi.h"
#include "gnc-backend-dbi.hpp"
#include <gnc-sql-object-backend.hpp>
#include "gnc-dbisqlresult.hpp"
#include "gnc-dbisqlconnection.hpp"
#if PLATFORM(WINDOWS)
#ifdef __STRICT_ANSI_UNSET__
#undef __STRICT_ANSI_UNSET__
#define __STRICT_ANSI__ 1
#endif
#endif
#if LIBDBI_VERSION >= 900
#define HAVE_LIBDBI_R 1
static dbi_inst dbi_instance = nullptr;
#else
#define HAVE_LIBDBI_R 0
#define HAVE_LIBDBI_TO_LONGLONG 0
#endif
#define TRANSACTION_NAME "trans"
static QofLogModule log_module = G_LOG_DOMAIN;
#define FILE_URI_TYPE "file"
#define FILE_URI_PREFIX (FILE_URI_TYPE "://")
#define SQLITE3_URI_TYPE "sqlite3"
#define SQLITE3_URI_PREFIX (SQLITE3_URI_TYPE "://")
#define PGSQL_DEFAULT_PORT 5432
static void adjust_sql_options (dbi_conn connection);
template<DbType Type> bool save_may_clobber_data (dbi_conn conn,
const std::string& dbname);
template <DbType Type>
class QofDbiBackendProvider : public QofBackendProvider
{
public:
QofDbiBackendProvider (const char* name, const char* type) :
QofBackendProvider {name, type} {}
QofDbiBackendProvider(QofDbiBackendProvider&) = delete;
QofDbiBackendProvider operator=(QofDbiBackendProvider&) = delete;
QofDbiBackendProvider(QofDbiBackendProvider&&) = delete;
QofDbiBackendProvider operator=(QofDbiBackendProvider&&) = delete;
~QofDbiBackendProvider () = default;
QofBackend* create_backend(void)
{
return new GncDbiBackend<Type>(nullptr, nullptr);
}
bool type_check(const char* type) { return true; }
};
/* ================================================================= */
/* ================================================================= */
struct UriStrings
{
UriStrings() = default;
UriStrings(const std::string& uri);
~UriStrings() = default;
std::string basename() const noexcept;
const char* dbname() const noexcept;
std::string quote_dbname(DbType t) const noexcept;
std::string m_protocol;
std::string m_host;
std::string m_dbname;
std::string m_username;
std::string m_password;
std::string m_basename;
int m_portnum;
};
UriStrings::UriStrings(const std::string& uri)
{
gchar *scheme, *host, *username, *password, *dbname;
int portnum;
gnc_uri_get_components(uri.c_str(), &scheme, &host, &portnum, &username,
&password, &dbname);
m_protocol = std::string{scheme};
m_host = std::string{host};
if (dbname)
m_dbname = std::string{dbname};
if (username)
m_username = std::string{username};
if (password)
m_password = std::string{password};
m_portnum = portnum;
g_free(scheme);
g_free(host);
g_free(username);
g_free(password);
g_free(dbname);
}
std::string
UriStrings::basename() const noexcept
{
return m_protocol + "_" + m_host + "_" + m_username + "_" + m_dbname;
}
const char*
UriStrings::dbname() const noexcept
{
return m_dbname.c_str();
}
std::string
UriStrings::quote_dbname(DbType t) const noexcept
{
if (m_dbname.empty())
return "";
const char quote = (t == DbType::DBI_MYSQL ? '`' : '"');
std::string retval(1, quote);
retval += m_dbname + quote;
return retval;
}
static void
set_options(dbi_conn conn, const PairVec& options)
{
for (auto option : options)
{
auto opt = option.first.c_str();
auto val = option.second.c_str();
auto result = dbi_conn_set_option(conn, opt, val);
if (result < 0)
{
const char *msg = nullptr;
dbi_conn_error(conn, &msg);
PERR("Error setting %s option to %s: %s", opt, val, msg);
throw std::runtime_error(msg);
}
}
}
/**
* Sets standard db options in a dbi_conn.
*
* @param conn dbi_conn connection
* @param uri UriStrings containing the needed parameters.
* @return TRUE if successful, FALSE if error
*/
template <DbType Type> bool
GncDbiBackend<Type>::set_standard_connection_options (dbi_conn conn,
const UriStrings& uri)
{
gint result;
PairVec options;
options.push_back(std::make_pair("host", uri.m_host));
options.push_back(std::make_pair("dbname", uri.m_dbname));
options.push_back(std::make_pair("username", uri.m_username));
options.push_back(std::make_pair("password", uri.m_password));
options.push_back(std::make_pair("encoding", "UTF-8"));
try
{
set_options(conn, options);
auto result = dbi_conn_set_option_numeric(conn, "port", uri.m_portnum);
if (result < 0)
{
const char *msg = nullptr;
auto err = dbi_conn_error(conn, &msg);
PERR("Error setting port option to %d: %s", uri.m_portnum, msg);
throw std::runtime_error(msg);
}
}
catch (std::runtime_error& err)
{
set_error (ERR_BACKEND_SERVER_ERR);
return false;
}
return true;
}
template <DbType Type> void error_handler(dbi_conn conn, void* data);
void error_handler(dbi_conn conn, void* data);
template <DbType Type> dbi_conn
GncDbiBackend<Type>::conn_setup (PairVec& options, UriStrings& uri)
{
const char* dbstr = (Type == DbType::DBI_SQLITE ? "sqlite3" :
Type == DbType::DBI_MYSQL ? "mysql" : "pgsql");
#if HAVE_LIBDBI_R
dbi_conn conn = nullptr;
if (dbi_instance)
conn = dbi_conn_new_r (dbstr, dbi_instance);
else
PERR ("Attempt to connect with an uninitialized dbi_instance");
#else
auto conn = dbi_conn_new (dbstr);
#endif
if (conn == nullptr)
{
PERR ("Unable to create %s dbi connection", dbstr);
set_error (ERR_BACKEND_BAD_URL);
return nullptr;
}
dbi_conn_error_handler (conn, error_handler<Type>, this);
if (!uri.m_dbname.empty() &&
!set_standard_connection_options(conn, uri))
{
dbi_conn_close(conn);
return nullptr;
}
if(!options.empty())
{
try {
set_options(conn, options);
}
catch (std::runtime_error& err)
{
dbi_conn_close(conn);
set_error (ERR_BACKEND_SERVER_ERR);
return nullptr;
}
}
return conn;
}
template <DbType Type>bool
GncDbiBackend<Type>::create_database(dbi_conn conn, const char* db)
{
const char *dbname;
const char *dbcreate;
if (Type == DbType::DBI_MYSQL)
{
dbname = "mysql";
dbcreate = "CREATE DATABASE %s CHARACTER SET utf8";
}
else
{
dbname = "postgres";
dbcreate = "CREATE DATABASE %s WITH TEMPLATE template0 ENCODING 'UTF8'";
}
PairVec options;
options.push_back(std::make_pair("dbname", dbname));
try
{
set_options(conn, options);
}
catch (std::runtime_error& err)
{
set_error (ERR_BACKEND_SERVER_ERR);
return false;
}
auto result = dbi_conn_connect (conn);
if (result < 0)
{
PERR ("Unable to connect to %s database", dbname);
set_error(ERR_BACKEND_SERVER_ERR);
return false;
}
if (Type == DbType::DBI_MYSQL)
adjust_sql_options(conn);
auto dresult = dbi_conn_queryf (conn, dbcreate, db);
if (dresult == nullptr)
{
PERR ("Unable to create database '%s'\n", db);
set_error (ERR_BACKEND_SERVER_ERR);
return false;
}
if (Type == DbType::DBI_PGSQL)
{
const char *alterdb = "ALTER DATABASE %s SET "
"standard_conforming_strings TO on";
dbi_conn_queryf (conn, alterdb, db);
}
dbi_conn_close(conn);
conn = nullptr;
return true;
}
template <> void
error_handler<DbType::DBI_SQLITE> (dbi_conn conn, void* user_data)
{
const char* msg;
GncDbiBackend<DbType::DBI_SQLITE> *dbi_be =
static_cast<decltype(dbi_be)>(user_data);
int err_num = dbi_conn_error (conn, &msg);
/* BADIDX is raised if we attempt to seek outside of a result. We
* handle that possibility after checking the return value of the
* seek. Having this raise a critical error breaks looping by
* testing for the return value of the seek.
*/
if (err_num == DBI_ERROR_BADIDX) return;
PERR ("DBI error: %s\n", msg);
if (dbi_be->connected())
dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, false);
}
template <> void
GncDbiBackend<DbType::DBI_SQLITE>::session_begin(QofSession* session,
const char* book_id,
bool ignore_lock,
bool create, bool force)
{
gboolean file_exists;
PairVec options;
g_return_if_fail (session != nullptr);
g_return_if_fail (book_id != nullptr);
ENTER (" ");
/* Remove uri type if present */
auto path = gnc_uri_get_path (book_id);
std::string filepath{path};
g_free(path);
GFileTest ftest = static_cast<decltype (ftest)> (
G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS) ;
file_exists = g_file_test (filepath.c_str(), ftest);
if (!create && !file_exists)
{
set_error (ERR_FILEIO_FILE_NOT_FOUND);
std::string msg{"Sqlite3 file "};
set_message (msg + filepath + " not found");
PWARN ("Sqlite3 file %s not found", filepath.c_str());
LEAVE("Error");
return;
}
if (create && file_exists)
{
if (force)
g_unlink (filepath.c_str());
else
{
set_error (ERR_BACKEND_STORE_EXISTS);
auto msg = "Might clobber, no force";
PWARN ("%s", msg);
LEAVE("Error");
return;
}
}
connect(nullptr);
/* dbi-sqlite3 documentation says that sqlite3 doesn't take a "host" option */
options.push_back(std::make_pair("host", "localhost"));
auto dirname = g_path_get_dirname (filepath.c_str());
auto basename = g_path_get_basename (filepath.c_str());
options.push_back(std::make_pair("dbname", basename));
options.push_back(std::make_pair("sqlite3_dbdir", dirname));
if (basename != nullptr) g_free (basename);
if (dirname != nullptr) g_free (dirname);
UriStrings uri;
auto conn = conn_setup(options, uri);
if (conn == nullptr)
{
LEAVE("Error");
return;
}
auto result = dbi_conn_connect (conn);
if (result < 0)
{
dbi_conn_close(conn);
PERR ("Unable to connect to %s: %d\n", book_id, result);
set_error (ERR_BACKEND_BAD_URL);
LEAVE("Error");
return;
}
if (!conn_test_dbi_library(conn))
{
if (create && !file_exists)
{
/* File didn't exist before, but it does now, and we don't want to
* leave it lying around.
*/
dbi_conn_close (conn);
conn = nullptr;
g_unlink (filepath.c_str());
}
dbi_conn_close(conn);
LEAVE("Bad DBI Library");
return;
}
try
{
connect(new GncDbiSqlConnection(DbType::DBI_SQLITE,
this, conn, ignore_lock));
}
catch (std::runtime_error& err)
{
return;
}
/* We should now have a proper session set up.
* Let's start logging */
xaccLogSetBaseName (filepath.c_str());
PINFO ("logpath=%s", filepath.c_str() ? filepath.c_str() : "(null)");
LEAVE ("");
}
template <> void
error_handler<DbType::DBI_MYSQL> (dbi_conn conn, void* user_data)
{
GncDbiBackend<DbType::DBI_MYSQL>* dbi_be =
static_cast<decltype(dbi_be)>(user_data);
const char* msg;
auto err_num = dbi_conn_error (conn, &msg);
/* BADIDX is raised if we attempt to seek outside of a result. We
* handle that possibility after checking the return value of the
* seek. Having this raise a critical error breaks looping by
* testing for the return value of the seek.
*/
if (err_num == DBI_ERROR_BADIDX) return;
/* Note: the sql connection may not have been initialized yet
* so let's be careful with using it
*/
/* Database doesn't exist. When this error is triggered the
* GncDbiSqlConnection may not exist yet either, so don't use it here
*/
if (err_num == 1049) // Database doesn't exist
{
PINFO ("DBI error: %s\n", msg);
dbi_be->set_exists(false);
return;
}
/* All the other error handling code assumes the GncDbiSqlConnection
* has been initialized. So let's assert it exits here, otherwise
* simply return.
*/
if (!dbi_be->connected())
{
PINFO ("DBI error: %s\n", msg);
PINFO ("Note: GbcDbiSqlConnection not yet initialized. Skipping further error processing.");
return;
}
/* Test for other errors */
if (err_num == 2006) // Server has gone away
{
PINFO ("DBI error: %s - Reconnecting...\n", msg);
dbi_be->set_dbi_error (ERR_BACKEND_CONN_LOST, 1, true);
dbi_be->retry_connection(msg);
}
else if (err_num == 2003) // Unable to connect
{
dbi_be->set_dbi_error (ERR_BACKEND_CANT_CONNECT, 1, true);
dbi_be->retry_connection (msg);
}
else if (err_num == 1007) //Database exists
{
dbi_be->set_exists(true);
return;
}
else // Any other error
{
PERR ("DBI error: %s\n", msg);
dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, FALSE);
}
}
#define SQL_OPTION_TO_REMOVE "NO_ZERO_DATE"
/* Given an sql_options string returns a copy of the string adjusted as
* necessary. In particular if string the contains SQL_OPTION_TO_REMOVE it is
* removed along with comma separator.
*/
std::string
adjust_sql_options_string(const std::string& str)
{
/* Regex that finds the SQL_OPTION_TO_REMOVE as the first, last, or middle of a
* comma-delimited list.
*/
boost::regex reg{"(?:," SQL_OPTION_TO_REMOVE "$|\\b"
SQL_OPTION_TO_REMOVE "\\b,?)"};
return regex_replace(str, reg, std::string{""});
}
/* checks mysql sql_options and adjusts if necessary */
static void
adjust_sql_options (dbi_conn connection)
{
dbi_result result = dbi_conn_query( connection, "SELECT @@sql_mode");
if (result == nullptr)
{
const char* errmsg;
int err = dbi_conn_error(connection, &errmsg);
PERR("Unable to read sql_mode %d : %s", err, errmsg);
return;
}
dbi_result_first_row(result);
std::string str{dbi_result_get_string_idx(result, 1)};
dbi_result_free(result);
if (str.empty())
{
const char* errmsg;
int err = dbi_conn_error(connection, &errmsg);
if (err)
PERR("Unable to get sql_mode %d : %s", err, errmsg);
else
PINFO("Sql_mode isn't set.");
return;
}
PINFO("Initial sql_mode: %s", str.c_str());
if(str.find(SQL_OPTION_TO_REMOVE) == std::string::npos)
return;
std::string adjusted_str{adjust_sql_options_string(str)};
PINFO("Setting sql_mode to %s", adjusted_str.c_str());
std::string set_str{"SET sql_mode='" + std::move(adjusted_str) + "'"};
dbi_result set_result = dbi_conn_query(connection,
set_str.c_str());
if (set_result)
{
dbi_result_free(set_result);
}
else
{
const char* errmsg;
int err = dbi_conn_error(connection, &errmsg);
PERR("Unable to set sql_mode %d : %s", err, errmsg);
}
}
template <DbType Type> void
GncDbiBackend<Type>::session_begin (QofSession* session, const char* book_id,
bool ignore_lock, bool create, bool force)
{
GncDbiTestResult dbi_test_result = GNC_DBI_PASS;
PairVec options;
g_return_if_fail (session != nullptr);
g_return_if_fail (book_id != nullptr);
ENTER (" ");
/* Split the book-id
* Format is protocol://username:password@hostname:port/dbname
where username, password and port are optional) */
UriStrings uri(book_id);
if (Type == DbType::DBI_PGSQL)
{
if (uri.m_portnum == 0)
uri.m_portnum = PGSQL_DEFAULT_PORT;
/* Postgres's SQL interface coerces identifiers to lower case, but the
* C interface is case-sensitive. This results in a mixed-case dbname
* being created (with a lower case name) but then dbi can't conect to
* it. To work around this, coerce the name to lowercase first. */
auto lcname = g_utf8_strdown (uri.dbname(), -1);
uri.m_dbname = std::string{lcname};
g_free(lcname);
}
connect(nullptr);
auto conn = conn_setup(options, uri);
if (conn == nullptr)
{
LEAVE("Error");
return;
}
m_exists = true; //May be unset in the error handler.
auto result = dbi_conn_connect (conn);
if (result == 0)
{
if (Type == DbType::DBI_MYSQL)
adjust_sql_options (conn);
if(!conn_test_dbi_library(conn))
{
dbi_conn_close(conn);
LEAVE("Error");
return;
}
if (create && save_may_clobber_data<Type>(conn,
uri.quote_dbname(Type)))
{
if (force)
{
// Drop DB
const char *root_db;
if (Type == DbType::DBI_PGSQL)
{
root_db = "template1";
}
else if (Type == DbType::DBI_MYSQL)
{
root_db = "mysql";
}
else
{
PERR ("Unknown database type, can't proceed.");
LEAVE("Error");
return;
}
if (dbi_conn_select_db (conn, root_db) == -1)
{
PERR ("Failed to switch out of %s, drop will fail.",
uri.quote_dbname(Type).c_str());
LEAVE ("Error");
return;
}
if (!dbi_conn_queryf (conn, "DROP DATABASE %s",
uri.quote_dbname(Type).c_str()))
{
PERR ("Failed to drop database %s prior to recreating it."
"Proceeding would combine old and new data.",
uri.quote_dbname(Type).c_str());
LEAVE ("Error");
return;
}
}
else
{
set_error (ERR_BACKEND_STORE_EXISTS);
PWARN ("Database already exists, Might clobber it.");
dbi_conn_close(conn);
LEAVE("Error");
return;
}
/* Drop successful. */
m_exists = false;
}
}
else if (m_exists)
{
PERR ("Unable to connect to database '%s'\n", uri.dbname());
set_error (ERR_BACKEND_SERVER_ERR);
dbi_conn_close(conn);
LEAVE("Error");
return;
}
else if (!create)
{
PERR ("Database '%s' does not exist\n", uri.dbname());
set_error(ERR_BACKEND_NO_SUCH_DB);
std::string msg{"Database "};
set_message(msg + uri.dbname() + " not found");
LEAVE("Error");
return;
}
if (create)
{
if (!m_exists &&
!create_database(conn, uri.quote_dbname(Type).c_str()))
{
dbi_conn_close(conn);
LEAVE("Error");
return;
}
conn = conn_setup(options, uri);
result = dbi_conn_connect (conn);
if (result < 0)
{
PERR ("Unable to create database '%s'\n", uri.dbname());
set_error (ERR_BACKEND_SERVER_ERR);
dbi_conn_close(conn);
LEAVE("Error");
return;
}
if (Type == DbType::DBI_MYSQL)
adjust_sql_options (conn);
if (!conn_test_dbi_library(conn))
{
if (Type == DbType::DBI_PGSQL)
dbi_conn_select_db (conn, "template1");
dbi_conn_queryf (conn, "DROP DATABASE %s",
uri.quote_dbname(Type).c_str());
dbi_conn_close(conn);
return;
}
}
connect(nullptr);
try
{
connect(new GncDbiSqlConnection(Type, this, conn, ignore_lock));
}
catch (std::runtime_error& err)
{
return;
}
/* We should now have a proper session set up.
* Let's start logging */
auto translog_path = gnc_build_translog_path (uri.basename().c_str());
xaccLogSetBaseName (translog_path);
PINFO ("logpath=%s", translog_path ? translog_path : "(null)");
g_free (translog_path);
LEAVE (" ");
}
template<> void
error_handler<DbType::DBI_PGSQL> (dbi_conn conn, void* user_data)
{
GncDbiBackend<DbType::DBI_PGSQL>* dbi_be =
static_cast<decltype(dbi_be)>(user_data);
const char* msg;
auto err_num = dbi_conn_error (conn, &msg);
/* BADIDX is raised if we attempt to seek outside of a result. We
* handle that possibility after checking the return value of the
* seek. Having this raise a critical error breaks looping by
* testing for the return value of the seek.
*/
if (err_num == DBI_ERROR_BADIDX) return;
if (g_str_has_prefix (msg, "FATAL: database") &&
g_str_has_suffix (msg, "does not exist\n"))
{
PINFO ("DBI error: %s\n", msg);
dbi_be->set_exists(false);
}
else if (g_strrstr (msg,
"server closed the connection unexpectedly")) // Connection lost
{
if (!dbi_be->connected())
{
PWARN ("DBI Error: Connection lost, connection pointer invalid");
return;
}
PINFO ("DBI error: %s - Reconnecting...\n", msg);
dbi_be->set_dbi_error (ERR_BACKEND_CONN_LOST, 1, true);
dbi_be->retry_connection(msg);
}
else if (g_str_has_prefix (msg, "connection pointer is NULL") ||
g_str_has_prefix (msg, "could not connect to server")) // No connection
{
if (!dbi_be->connected())
qof_backend_set_error(reinterpret_cast<QofBackend*>(dbi_be),
ERR_BACKEND_CANT_CONNECT);
else
{
dbi_be->set_dbi_error(ERR_BACKEND_CANT_CONNECT, 1, true);
dbi_be->retry_connection (msg);
}
}
else
{
PERR ("DBI error: %s\n", msg);
if (dbi_be->connected())
dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, false);
}
}
/* ================================================================= */
template <DbType Type> void
GncDbiBackend<Type>::session_end ()
{
ENTER (" ");
finalize_version_info ();
connect(nullptr);
LEAVE (" ");
}
template <DbType Type>
GncDbiBackend<Type>::~GncDbiBackend()
{
/* Stop transaction logging */
xaccLogSetBaseName (nullptr);
}
/* ================================================================= */
/* GNUCASH_RESAVE_VERSION indicates the earliest database version
* compatible with this version of Gnucash; the stored value is the
* earliest version of Gnucash conpatible with the database. If the
* GNUCASH_RESAVE_VERSION for this Gnucash is newer than the Gnucash
* version which created the database, a resave is offered. If the
* version of this Gnucash is older than the saved resave version,
* then the database will be loaded read-only. A resave will update
* both values to match this version of Gnucash.
*/
template <DbType Type> void
GncDbiBackend<Type>::load (QofBook* book, QofBackendLoadType loadType)
{
g_return_if_fail (book != nullptr);
ENTER ("dbi_be=%p, book=%p", this, book);
if (loadType == LOAD_TYPE_INITIAL_LOAD)
{
// Set up table version information
init_version_info ();
assert (m_book == nullptr);
create_tables();
}
GncSqlBackend::load(book, loadType);
if (Type == DbType::DBI_SQLITE)
gnc_features_set_used(book, GNC_FEATURE_SQLITE3_ISO_DATES);
if (GNUCASH_RESAVE_VERSION > get_table_version("Gnucash"))
{
/* The database was loaded with an older database schema or
* data semantics. In order to ensure consistency, the whole
* thing needs to be saved anew. */
set_error(ERR_SQL_DB_TOO_OLD);
}
else if (GNUCASH_RESAVE_VERSION < get_table_version("Gnucash-Resave"))
{
/* Worse, the database was created with a newer version. We
* can't safely write to this database, so the user will have
* to do a "save as" to make one that we can write to.
*/
set_error(ERR_SQL_DB_TOO_NEW);
}
LEAVE ("");
}
/* ================================================================= */
/* This is used too early to call GncDbiProvider::get_table_list(). */
template <DbType T> bool
save_may_clobber_data (dbi_conn conn, const std::string& dbname)
{
/* Data may be clobbered iff the number of tables != 0 */
auto result = dbi_conn_get_table_list (conn, dbname.c_str(), nullptr);
bool retval = false;
if (result)
{
retval = dbi_result_get_numrows (result) > 0;
dbi_result_free (result);
}
return retval;
}
template <> bool
save_may_clobber_data <DbType::DBI_PGSQL>(dbi_conn conn,
const std::string& dbname)
{
/* Data may be clobbered iff the number of tables != 0 */
const char* query = "SELECT relname FROM pg_class WHERE relname !~ '^(pg|sql)_' AND relkind = 'r' ORDER BY relname";
auto result = dbi_conn_query (conn, query);
bool retval = false;
if (result)
{
retval = dbi_result_get_numrows (result) > 0;
dbi_result_free (result);
}
return retval;
}
/**
* Safely resave a database by renaming all of its tables, recreating
* everything, and then dropping the backup tables only if there were
* no errors. If there are errors, drop the new tables and restore the
* originals.
*
* @param book: QofBook to be saved in the database.
*/
template <DbType Type> void
GncDbiBackend<Type>::safe_sync (QofBook* book)
{
auto conn = dynamic_cast<GncDbiSqlConnection*>(m_conn);
g_return_if_fail (conn != nullptr);
g_return_if_fail (book != nullptr);
ENTER ("book=%p, primary=%p", book, m_book);
if (!conn->begin_transaction())
{
LEAVE("Failed to obtain a transaction.");
return;
}
if (!conn->table_operation (TableOpType::backup))
{
conn->rollback_transaction();
LEAVE ("Failed to rename tables");
return;
}
if (!conn->drop_indexes())
{
conn->rollback_transaction();
LEAVE ("Failed to drop indexes");
return;
}
sync(m_book);
if (check_error())
{
conn->rollback_transaction();
LEAVE ("Failed to create new database tables");
return;
}
conn->table_operation (TableOpType::drop_backup);
conn->commit_transaction();
LEAVE ("book=%p", m_book);
}
/* MySQL commits the transaction and all savepoints after the first CREATE
* TABLE, crashing when we try to RELEASE SAVEPOINT because the savepoint
* doesn't exist after the commit. We must run without a wrapping transaction in
* that case.
*/
template <> void
GncDbiBackend<DbType::DBI_MYSQL>::safe_sync (QofBook* book)
{
auto conn = dynamic_cast<GncDbiSqlConnection*>(m_conn);
g_return_if_fail (conn != nullptr);
g_return_if_fail (book != nullptr);
ENTER ("book=%p, primary=%p", book, m_book);
if (!conn->table_operation (TableOpType::backup))
{
set_error(ERR_BACKEND_SERVER_ERR);
conn->table_operation (TableOpType::rollback);
LEAVE ("Failed to rename tables");
return;
}
if (!conn->drop_indexes())
{
conn->table_operation (TableOpType::rollback);
set_error (ERR_BACKEND_SERVER_ERR);
set_message("Failed to drop indexes");
LEAVE ("Failed to drop indexes");
return;
}
sync(m_book);
if (check_error())
{
conn->table_operation (TableOpType::rollback);
LEAVE ("Failed to create new database tables");
return;
}
conn->table_operation (TableOpType::drop_backup);
LEAVE ("book=%p", m_book);
}
/* ================================================================= */
/*
* Checks to see whether the file is an sqlite file or not
*
*/
template<> bool
QofDbiBackendProvider<DbType::DBI_SQLITE>::type_check(const char *uri)
{
FILE* f;
gchar buf[50];
G_GNUC_UNUSED size_t chars_read;
gint status;
gchar* filename;
// BAD if the path is null
g_return_val_if_fail (uri != nullptr, FALSE);
filename = gnc_uri_get_path (uri);
f = g_fopen (filename, "r");
g_free (filename);
// OK if the file doesn't exist - new file
if (f == nullptr)
{
PINFO ("doesn't exist (errno=%d) -> DBI", errno);
return TRUE;
}
// OK if file has the correct header
chars_read = fread (buf, sizeof (buf), 1, f);
status = fclose (f);
if (status < 0)
{
PERR ("Error in fclose(): %d\n", errno);
}
if (g_str_has_prefix (buf, "SQLite format 3"))
{
PINFO ("has SQLite format string -> DBI");
return TRUE;
}
PINFO ("exists, does not have SQLite format string -> not DBI");
// Otherwise, BAD
return FALSE;
}
void
gnc_module_init_backend_dbi (void)
{
const char* driver_dir;
int num_drivers;
gboolean have_sqlite3_driver = FALSE;
gboolean have_mysql_driver = FALSE;
gboolean have_pgsql_driver = FALSE;
/* Initialize libdbi and see which drivers are available. Only register qof backends which
have drivers available. */
driver_dir = g_getenv ("GNC_DBD_DIR");
if (driver_dir == nullptr)
{
PINFO ("GNC_DBD_DIR not set: using libdbi built-in default\n");
}
/* dbi_initialize returns -1 in case of errors */
#if HAVE_LIBDBI_R
if (dbi_instance)
return;
num_drivers = dbi_initialize_r (driver_dir, &dbi_instance);
#else
num_drivers = dbi_initialize (driver_dir);
#endif
if (num_drivers <= 0)
{
gchar* dir = g_build_filename (gnc_path_get_libdir (), "dbd", nullptr);
#if HAVE_LIBDBI_R
if (dbi_instance)
return;
num_drivers = dbi_initialize_r (dir, &dbi_instance);
#else
num_drivers = dbi_initialize (dir);
#endif
g_free (dir);
}
if (num_drivers <= 0)
{
PWARN ("No DBD drivers found\n");
}
else
{
dbi_driver driver = nullptr;
PINFO ("%d DBD drivers found\n", num_drivers);
do
{
#if HAVE_LIBDBI_R
driver = dbi_driver_list_r (driver, dbi_instance);
#else
driver = dbi_driver_list (driver);
#endif
if (driver != nullptr)
{
const gchar* name = dbi_driver_get_name (driver);
PINFO ("Driver: %s\n", name);
if (strcmp (name, "sqlite3") == 0)
{
have_sqlite3_driver = TRUE;
}
else if (strcmp (name, "mysql") == 0)
{
have_mysql_driver = TRUE;
}
else if (strcmp (name, "pgsql") == 0)
{
have_pgsql_driver = TRUE;
}
}
}
while (driver != nullptr);
}
if (have_sqlite3_driver)
{
const char* name = "GnuCash Libdbi (SQLITE3) Backend";
auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_SQLITE>{name, FILE_URI_TYPE});
qof_backend_register_provider(std::move(prov));
prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_SQLITE>{name, SQLITE3_URI_TYPE});
qof_backend_register_provider(std::move(prov));
}
if (have_mysql_driver)
{
const char *name = "GnuCash Libdbi (MYSQL) Backend";
auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_MYSQL>{name, "mysql"});
qof_backend_register_provider(std::move(prov));
}
if (have_pgsql_driver)
{
const char* name = "GnuCash Libdbi (POSTGRESQL) Backend";
auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_PGSQL>{name, "postgres"});
qof_backend_register_provider(std::move(prov));
}
/* If needed, set log level to DEBUG so that SQl statements will be put into
the gnucash.trace file. */
/* qof_log_set_level( log_module, QOF_LOG_DEBUG ); */
}
#ifndef GNC_NO_LOADABLE_MODULES
G_MODULE_EXPORT void
qof_backend_module_init (void)
{
gnc_module_init_backend_dbi ();
}
G_MODULE_EXPORT void
qof_backend_module_finalize (void)
{
gnc_module_finalize_backend_dbi ();
}
#endif /* GNC_NO_LOADABLE_MODULES */
void
gnc_module_finalize_backend_dbi (void)
{
#if HAVE_LIBDBI_R
if (dbi_instance)
{
dbi_shutdown_r (dbi_instance);
dbi_instance = nullptr;
}
#else
dbi_shutdown ();
#endif
}
/* --------------------------------------------------------- */
/** Users discovered a bug in some distributions of libdbi, where if
* it is compiled on certain versions of gcc with the -ffast-math
* compiler option it fails to correctly handle saving of 64-bit
* values. This function tests for the problem.
* @param: conn: The just-opened dbi_conn
* @returns: GNC_DBI_PASS if the dbi library is safe to use,
* GNC_DBI_FAIL_SETUP if the test could not be completed, or
* GNC_DBI_FAIL_TEST if the bug was found.
*/
static GncDbiTestResult
dbi_library_test (dbi_conn conn)
{
int64_t testlonglong = -9223372036854775807LL, resultlonglong = 0;
uint64_t testulonglong = 9223372036854775807LLU, resultulonglong = 0;
double testdouble = 1.7976921348623157E+307, resultdouble = 0.0;
dbi_result result;
GncDbiTestResult retval = GNC_DBI_PASS;
result = dbi_conn_query (conn, "CREATE TEMPORARY TABLE numtest "
"( test_int BIGINT, test_unsigned BIGINT,"
" test_double FLOAT8 )");
if (result == nullptr)
{
PWARN ("Test_DBI_Library: Create table failed");
return GNC_DBI_FAIL_SETUP;
}
dbi_result_free (result);
std::stringstream querystr;
querystr << "INSERT INTO numtest VALUES (" << testlonglong <<
", " << testulonglong << ", " << std::setprecision(12) <<
testdouble << ")";
auto query = querystr.str();
result = dbi_conn_query (conn, query.c_str());
if (result == nullptr)
{
PWARN ("Test_DBI_Library: Failed to insert test row into table");
return GNC_DBI_FAIL_SETUP;
}
dbi_result_free (result);
auto locale = gnc_push_locale (LC_NUMERIC, "C");
result = dbi_conn_query (conn, "SELECT * FROM numtest");
if (result == nullptr)
{
const char* errmsg;
dbi_conn_error (conn, &errmsg);
PWARN ("Test_DBI_Library: Failed to retrieve test row into table: %s",
errmsg);
dbi_conn_query (conn, "DROP TABLE numtest");
gnc_pop_locale (LC_NUMERIC, locale);
return GNC_DBI_FAIL_SETUP;
}
while (dbi_result_next_row (result))
{
resultlonglong = dbi_result_get_longlong (result, "test_int");
resultulonglong = dbi_result_get_ulonglong (result, "test_unsigned");
resultdouble = dbi_result_get_double (result, "test_double");
}
dbi_conn_query (conn, "DROP TABLE numtest");
gnc_pop_locale (LC_NUMERIC, locale);
if (testlonglong != resultlonglong)
{
PWARN ("Test_DBI_Library: LongLong Failed %" PRId64 " != % " PRId64,
testlonglong, resultlonglong);
retval = GNC_DBI_FAIL_TEST;
}
if (testulonglong != resultulonglong)
{
PWARN ("Test_DBI_Library: Unsigned longlong Failed %" PRIu64 " != %"
PRIu64, testulonglong, resultulonglong);
retval = GNC_DBI_FAIL_TEST;
}
/* A bug in libdbi stores only 7 digits of precision */
if (testdouble >= resultdouble + 0.000001e307 ||
testdouble <= resultdouble - 0.000001e307)
{
PWARN ("Test_DBI_Library: Double Failed %17e != %17e",
testdouble, resultdouble);
retval = GNC_DBI_FAIL_TEST;
}
return retval;
}
template <DbType Type> bool
GncDbiBackend<Type>::conn_test_dbi_library(dbi_conn conn)
{
auto result = dbi_library_test (conn);
switch (result)
{
case GNC_DBI_PASS:
break;
case GNC_DBI_FAIL_SETUP:
set_error(ERR_SQL_DBI_UNTESTABLE);
set_message ("DBI library large number test incomplete");
break;
case GNC_DBI_FAIL_TEST:
set_error (ERR_SQL_BAD_DBI);
set_message ("DBI library fails large number test");
break;
}
return result == GNC_DBI_PASS;
}
/* ========================== END OF FILE ===================== */