mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
770 lines
23 KiB
C++
770 lines
23 KiB
C++
/********************************************************************
|
|
* gnc-dbisqlconnection.cpp: Encapsulate libdbi dbi_conn *
|
|
* *
|
|
* Copyright 2016 John Ralls <jralls@ceridwen.us> *
|
|
* *
|
|
* 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 *
|
|
\********************************************************************/
|
|
|
|
#include <guid.hpp>
|
|
extern "C"
|
|
{
|
|
#include <config.h>
|
|
#include <platform.h>
|
|
#include <gnc-locale-utils.h>
|
|
}
|
|
|
|
#include <string>
|
|
#include <regex>
|
|
#include <sstream>
|
|
|
|
#include "gnc-dbisqlconnection.hpp"
|
|
|
|
static QofLogModule log_module = G_LOG_DOMAIN;
|
|
// gnc-dbiproviderimpl.hpp has templates that need log_module defined.
|
|
#include "gnc-dbiproviderimpl.hpp"
|
|
|
|
static const unsigned int DBI_MAX_CONN_ATTEMPTS = 5;
|
|
const std::string lock_table = "gnclock";
|
|
|
|
/* --------------------------------------------------------- */
|
|
class GncDbiSqlStatement : public GncSqlStatement
|
|
{
|
|
public:
|
|
GncDbiSqlStatement(const GncSqlConnection* conn, const std::string& sql) :
|
|
m_conn{conn}, m_sql {sql} {}
|
|
~GncDbiSqlStatement() {}
|
|
const char* to_sql() const override;
|
|
void add_where_cond(QofIdTypeConst, const PairVec&) override;
|
|
|
|
private:
|
|
const GncSqlConnection* m_conn = nullptr;
|
|
std::string m_sql;
|
|
};
|
|
|
|
|
|
const char*
|
|
GncDbiSqlStatement::to_sql() const
|
|
{
|
|
return m_sql.c_str();
|
|
}
|
|
|
|
void
|
|
GncDbiSqlStatement::add_where_cond(QofIdTypeConst type_name,
|
|
const PairVec& col_values)
|
|
{
|
|
m_sql += " WHERE ";
|
|
for (auto colpair : col_values)
|
|
{
|
|
if (colpair != *col_values.begin())
|
|
m_sql += " AND ";
|
|
if (colpair.second == "NULL")
|
|
m_sql += colpair.first + " IS " + colpair.second;
|
|
else
|
|
m_sql += colpair.first + " = " + colpair.second;
|
|
}
|
|
}
|
|
|
|
GncDbiSqlConnection::GncDbiSqlConnection (DbType type, QofBackend* qbe,
|
|
dbi_conn conn, SessionOpenMode mode) :
|
|
m_qbe{qbe}, m_conn{conn},
|
|
m_provider{type == DbType::DBI_SQLITE ?
|
|
make_dbi_provider<DbType::DBI_SQLITE>() :
|
|
type == DbType::DBI_MYSQL ?
|
|
make_dbi_provider<DbType::DBI_MYSQL>() :
|
|
make_dbi_provider<DbType::DBI_PGSQL>()},
|
|
m_conn_ok{true}, m_last_error{ERR_BACKEND_NO_ERR}, m_error_repeat{0},
|
|
m_retry{false}, m_sql_savepoint{0}, m_readonly{false}
|
|
{
|
|
if (mode == SESSION_READ_ONLY)
|
|
m_readonly = true;
|
|
else if (!lock_database(mode == SESSION_BREAK_LOCK))
|
|
throw std::runtime_error("Failed to lock database!");
|
|
if (!check_and_rollback_failed_save())
|
|
{
|
|
unlock_database();
|
|
throw std::runtime_error("A failed safe-save was detected and rolling it back failed.");
|
|
}
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::lock_database (bool break_lock)
|
|
{
|
|
const char *errstr;
|
|
/* Protect everything with a single transaction to prevent races */
|
|
if (!begin_transaction())
|
|
return false;
|
|
auto tables = m_provider->get_table_list(m_conn, lock_table);
|
|
if (tables.empty())
|
|
{
|
|
auto result = dbi_conn_queryf (m_conn,
|
|
"CREATE TABLE %s ( Hostname varchar(%d), PID int )",
|
|
lock_table.c_str(),
|
|
GNC_HOST_NAME_MAX);
|
|
if (result)
|
|
{
|
|
dbi_result_free (result);
|
|
result = nullptr;
|
|
}
|
|
if (dbi_conn_error (m_conn, &errstr))
|
|
{
|
|
PERR ("Error %s creating lock table", errstr);
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Check for an existing entry; delete it if break_lock is true, otherwise fail */
|
|
char hostname[ GNC_HOST_NAME_MAX + 1 ];
|
|
auto result = dbi_conn_queryf (m_conn, "SELECT * FROM %s",
|
|
lock_table.c_str());
|
|
if (result && dbi_result_get_numrows (result))
|
|
{
|
|
dbi_result_free (result);
|
|
result = nullptr;
|
|
if (!break_lock)
|
|
{
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_LOCKED);
|
|
/* FIXME: After enhancing the qof_backend_error mechanism, report in the dialog what is the hostname of the machine holding the lock. */
|
|
rollback_transaction();
|
|
return false;
|
|
}
|
|
result = dbi_conn_queryf (m_conn, "DELETE FROM %s", lock_table.c_str());
|
|
if (!result)
|
|
{
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
m_qbe->set_message("Failed to delete lock record");
|
|
rollback_transaction();
|
|
return false;
|
|
}
|
|
dbi_result_free (result);
|
|
result = nullptr;
|
|
}
|
|
/* Add an entry and commit the transaction */
|
|
memset (hostname, 0, sizeof (hostname));
|
|
gethostname (hostname, GNC_HOST_NAME_MAX);
|
|
result = dbi_conn_queryf (m_conn,
|
|
"INSERT INTO %s VALUES ('%s', '%d')",
|
|
lock_table.c_str(), hostname, (int)GETPID ());
|
|
if (!result)
|
|
{
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
m_qbe->set_message("Failed to create lock record");
|
|
rollback_transaction();
|
|
return false;
|
|
}
|
|
dbi_result_free (result);
|
|
return commit_transaction();
|
|
}
|
|
|
|
void
|
|
GncDbiSqlConnection::unlock_database ()
|
|
{
|
|
if (m_conn == nullptr) return;
|
|
if (m_readonly) return;
|
|
g_return_if_fail (dbi_conn_error (m_conn, nullptr) == 0);
|
|
|
|
auto tables = m_provider->get_table_list (m_conn, lock_table);
|
|
if (tables.empty())
|
|
{
|
|
PWARN ("No lock table in database, so not unlocking it.");
|
|
return;
|
|
}
|
|
if (begin_transaction())
|
|
{
|
|
/* Delete the entry if it's our hostname and PID */
|
|
char hostname[ GNC_HOST_NAME_MAX + 1 ];
|
|
|
|
memset (hostname, 0, sizeof (hostname));
|
|
gethostname (hostname, GNC_HOST_NAME_MAX);
|
|
auto result = dbi_conn_queryf (m_conn,
|
|
"SELECT * FROM %s WHERE Hostname = '%s' "
|
|
"AND PID = '%d'", lock_table.c_str(),
|
|
hostname,
|
|
(int)GETPID ());
|
|
if (result && dbi_result_get_numrows (result))
|
|
{
|
|
if (result)
|
|
{
|
|
dbi_result_free (result);
|
|
result = nullptr;
|
|
}
|
|
result = dbi_conn_queryf (m_conn, "DELETE FROM %s",
|
|
lock_table.c_str());
|
|
if (!result)
|
|
{
|
|
PERR ("Failed to delete the lock entry");
|
|
m_qbe->set_error (ERR_BACKEND_SERVER_ERR);
|
|
rollback_transaction();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
dbi_result_free (result);
|
|
result = nullptr;
|
|
}
|
|
commit_transaction();
|
|
return;
|
|
}
|
|
rollback_transaction();
|
|
PWARN ("There was no lock entry in the Lock table");
|
|
return;
|
|
}
|
|
PWARN ("Unable to get a lock on LOCK, so failed to clear the lock entry.");
|
|
m_qbe->set_error (ERR_BACKEND_SERVER_ERR);
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::check_and_rollback_failed_save()
|
|
{
|
|
auto backup_tables = m_provider->get_table_list(m_conn, "%back");
|
|
if (backup_tables.empty())
|
|
return true;
|
|
auto merge_tables = m_provider->get_table_list(m_conn, "%_merge");
|
|
if (!merge_tables.empty())
|
|
{
|
|
PERR("Merge tables exist in the database indicating a previous"
|
|
"attempt to recover from a failed safe-save. Automatic"
|
|
"recovery is beyond GnuCash's ability, you must recover"
|
|
"by hand or restore from a good backup.");
|
|
return false;
|
|
}
|
|
return table_operation(recover);
|
|
}
|
|
|
|
GncDbiSqlConnection::~GncDbiSqlConnection()
|
|
{
|
|
if (m_conn)
|
|
{
|
|
unlock_database();
|
|
dbi_conn_close(m_conn);
|
|
m_conn = nullptr;
|
|
}
|
|
}
|
|
|
|
GncSqlResultPtr
|
|
GncDbiSqlConnection::execute_select_statement (const GncSqlStatementPtr& stmt)
|
|
noexcept
|
|
{
|
|
dbi_result result;
|
|
|
|
DEBUG ("SQL: %s\n", stmt->to_sql());
|
|
auto locale = gnc_push_locale (LC_NUMERIC, "C");
|
|
do
|
|
{
|
|
init_error ();
|
|
result = dbi_conn_query (m_conn, stmt->to_sql());
|
|
}
|
|
while (m_retry);
|
|
if (result == nullptr)
|
|
{
|
|
PERR ("Error executing SQL %s\n", stmt->to_sql());
|
|
if(m_last_error)
|
|
m_qbe->set_error(m_last_error);
|
|
else
|
|
m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
|
|
}
|
|
gnc_pop_locale (LC_NUMERIC, locale);
|
|
return GncSqlResultPtr(new GncDbiSqlResult (this, result));
|
|
}
|
|
|
|
int
|
|
GncDbiSqlConnection::execute_nonselect_statement (const GncSqlStatementPtr& stmt)
|
|
noexcept
|
|
{
|
|
dbi_result result;
|
|
|
|
DEBUG ("SQL: %s\n", stmt->to_sql());
|
|
do
|
|
{
|
|
init_error ();
|
|
result = dbi_conn_query (m_conn, stmt->to_sql());
|
|
}
|
|
while (m_retry);
|
|
if (result == nullptr && m_last_error)
|
|
{
|
|
PERR ("Error executing SQL %s\n", stmt->to_sql());
|
|
if(m_last_error)
|
|
m_qbe->set_error(m_last_error);
|
|
else
|
|
m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
|
|
return -1;
|
|
}
|
|
if (!result)
|
|
return 0;
|
|
auto num_rows = (gint)dbi_result_get_numrows_affected (result);
|
|
auto status = dbi_result_free (result);
|
|
if (status < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
if(m_last_error)
|
|
m_qbe->set_error(m_last_error);
|
|
else
|
|
m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
|
|
}
|
|
return num_rows;
|
|
}
|
|
|
|
GncSqlStatementPtr
|
|
GncDbiSqlConnection::create_statement_from_sql (const std::string& sql)
|
|
const noexcept
|
|
{
|
|
return std::unique_ptr<GncSqlStatement>{new GncDbiSqlStatement (this, sql)};
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::does_table_exist (const std::string& table_name)
|
|
const noexcept
|
|
{
|
|
return ! m_provider->get_table_list(m_conn, table_name).empty();
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::begin_transaction () noexcept
|
|
{
|
|
dbi_result result;
|
|
|
|
DEBUG ("BEGIN\n");
|
|
|
|
if (!verify ())
|
|
{
|
|
PERR ("gnc_dbi_verify_conn() failed\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
init_error ();
|
|
if (m_sql_savepoint == 0)
|
|
result = dbi_conn_queryf (m_conn, "BEGIN");
|
|
else
|
|
{
|
|
std::ostringstream savepoint;
|
|
savepoint << "savepoint_" << m_sql_savepoint;
|
|
result = dbi_conn_queryf(m_conn, "SAVEPOINT %s",
|
|
savepoint.str().c_str());
|
|
}
|
|
}
|
|
while (m_retry);
|
|
|
|
if (!result)
|
|
{
|
|
PERR ("BEGIN transaction failed()\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
if (dbi_result_free (result) < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
++m_sql_savepoint;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::rollback_transaction () noexcept
|
|
{
|
|
DEBUG ("ROLLBACK\n");
|
|
if (m_sql_savepoint == 0) return false;
|
|
dbi_result result;
|
|
if (m_sql_savepoint == 1)
|
|
result = dbi_conn_query (m_conn, "ROLLBACK");
|
|
else
|
|
{
|
|
std::ostringstream savepoint;
|
|
savepoint << "savepoint_" << m_sql_savepoint - 1;
|
|
result = dbi_conn_queryf(m_conn, "ROLLBACK TO SAVEPOINT %s",
|
|
savepoint.str().c_str());
|
|
}
|
|
if (!result)
|
|
{
|
|
PERR ("Error in conn_rollback_transaction()\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
|
|
if (dbi_result_free (result) < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
|
|
--m_sql_savepoint;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::commit_transaction () noexcept
|
|
{
|
|
DEBUG ("COMMIT\n");
|
|
if (m_sql_savepoint == 0) return false;
|
|
dbi_result result;
|
|
if (m_sql_savepoint == 1)
|
|
result = dbi_conn_queryf (m_conn, "COMMIT");
|
|
else
|
|
{
|
|
std::ostringstream savepoint;
|
|
savepoint << "savepoint_" << m_sql_savepoint - 1;
|
|
result = dbi_conn_queryf(m_conn, "RELEASE SAVEPOINT %s",
|
|
savepoint.str().c_str());
|
|
}
|
|
|
|
if (!result)
|
|
{
|
|
PERR ("Error in conn_commit_transaction()\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
|
|
if (dbi_result_free (result) < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
return false;
|
|
}
|
|
--m_sql_savepoint;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
GncDbiSqlConnection::create_table (const std::string& table_name,
|
|
const ColVec& info_vec) const noexcept
|
|
{
|
|
std::string ddl;
|
|
unsigned int col_num = 0;
|
|
|
|
ddl += "CREATE TABLE " + table_name + "(";
|
|
for (auto const& info : info_vec)
|
|
{
|
|
if (col_num++ != 0)
|
|
{
|
|
ddl += ", ";
|
|
}
|
|
m_provider->append_col_def (ddl, info);
|
|
}
|
|
ddl += ")";
|
|
|
|
if (ddl.empty())
|
|
return false;
|
|
|
|
DEBUG ("SQL: %s\n", ddl.c_str());
|
|
auto result = dbi_conn_query (m_conn, ddl.c_str());
|
|
auto status = dbi_result_free (result);
|
|
if (status < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static std::string
|
|
create_index_ddl (const GncSqlConnection* conn, const std::string& index_name,
|
|
const std::string& table_name, const EntryVec& col_table)
|
|
{
|
|
std::string ddl;
|
|
ddl += "CREATE INDEX " + index_name + " ON " + table_name + "(";
|
|
for (const auto& table_row : col_table)
|
|
{
|
|
if (table_row != *col_table.begin())
|
|
{
|
|
ddl =+ ", ";
|
|
}
|
|
ddl += table_row->name();
|
|
}
|
|
ddl += ")";
|
|
return ddl;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::create_index(const std::string& index_name,
|
|
const std::string& table_name,
|
|
const EntryVec& col_table) const noexcept
|
|
{
|
|
auto ddl = create_index_ddl (this, index_name, table_name, col_table);
|
|
if (ddl.empty())
|
|
return false;
|
|
DEBUG ("SQL: %s\n", ddl.c_str());
|
|
auto result = dbi_conn_query (m_conn, ddl.c_str());
|
|
auto status = dbi_result_free (result);
|
|
if (status < 0)
|
|
{
|
|
PERR ("Error in dbi_result_free() result\n");
|
|
qof_backend_set_error (m_qbe, ERR_BACKEND_SERVER_ERR);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::add_columns_to_table(const std::string& table_name,
|
|
const ColVec& info_vec)
|
|
const noexcept
|
|
{
|
|
auto ddl = add_columns_ddl(table_name, info_vec);
|
|
if (ddl.empty())
|
|
return false;
|
|
|
|
DEBUG ("SQL: %s\n", ddl.c_str());
|
|
auto result = dbi_conn_query (m_conn, ddl.c_str());
|
|
auto status = dbi_result_free (result);
|
|
if (status < 0)
|
|
{
|
|
PERR( "Error in dbi_result_free() result\n" );
|
|
qof_backend_set_error(m_qbe, ERR_BACKEND_SERVER_ERR );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string
|
|
GncDbiSqlConnection::quote_string (const std::string& unquoted_str)
|
|
const noexcept
|
|
{
|
|
char* quoted_str;
|
|
size_t size;
|
|
|
|
dbi_conn_quote_string_copy (m_conn, unquoted_str.c_str(),
|
|
"ed_str);
|
|
if (quoted_str == nullptr)
|
|
return std::string{""};
|
|
std::string retval{quoted_str};
|
|
free(quoted_str);
|
|
return retval;
|
|
}
|
|
|
|
|
|
/** Check if the dbi connection is valid. If not attempt to re-establish it
|
|
* Returns TRUE is there is a valid connection in the end or FALSE otherwise
|
|
*/
|
|
bool
|
|
GncDbiSqlConnection::verify () noexcept
|
|
{
|
|
if (m_conn_ok)
|
|
return true;
|
|
|
|
/* We attempt to connect only once here. The error function will
|
|
* automatically re-attempt up until DBI_MAX_CONN_ATTEMPTS time to connect
|
|
* if this call fails. After all these attempts, conn_ok will indicate if
|
|
* there is a valid connection or not.
|
|
*/
|
|
init_error ();
|
|
m_conn_ok = true;
|
|
(void)dbi_conn_connect (m_conn);
|
|
|
|
return m_conn_ok;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::retry_connection(const char* msg)
|
|
noexcept
|
|
{
|
|
while (m_retry && m_error_repeat <= DBI_MAX_CONN_ATTEMPTS)
|
|
{
|
|
m_conn_ok = false;
|
|
if (dbi_conn_connect(m_conn) == 0)
|
|
{
|
|
init_error();
|
|
m_conn_ok = true;
|
|
return true;
|
|
}
|
|
#ifdef G_OS_WIN32
|
|
const guint backoff_msecs = 1;
|
|
Sleep (backoff_msecs * 2 << ++m_error_repeat);
|
|
#else
|
|
const guint backoff_usecs = 1000;
|
|
usleep (backoff_usecs * 2 << ++m_error_repeat);
|
|
#endif
|
|
PINFO ("DBI error: %s - Reconnecting...\n", msg);
|
|
|
|
}
|
|
PERR ("DBI error: %s - Giving up after %d consecutive attempts.\n", msg,
|
|
DBI_MAX_CONN_ATTEMPTS);
|
|
m_conn_ok = false;
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::rename_table(const std::string& old_name,
|
|
const std::string& new_name)
|
|
{
|
|
std::string sql = "ALTER TABLE " + old_name + " RENAME TO " + new_name;
|
|
auto stmt = create_statement_from_sql(sql);
|
|
return execute_nonselect_statement(stmt) >= 0;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::drop_table(const std::string& table)
|
|
{
|
|
std::string sql = "DROP TABLE " + table;
|
|
auto stmt = create_statement_from_sql(sql);
|
|
return execute_nonselect_statement(stmt) >= 0;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::merge_tables(const std::string& table,
|
|
const std::string& other)
|
|
{
|
|
auto merge_table = table + "_merge";
|
|
std::string sql = "CREATE TABLE " + merge_table + " AS SELECT * FROM " +
|
|
table + " UNION SELECT * FROM " + other;
|
|
auto stmt = create_statement_from_sql(sql);
|
|
if (execute_nonselect_statement(stmt) < 0)
|
|
return false;
|
|
if (!drop_table(table))
|
|
return false;
|
|
if (!rename_table(merge_table, table))
|
|
return false;
|
|
return drop_table(other);
|
|
}
|
|
|
|
/**
|
|
* Perform a specified SQL operation on every table in a
|
|
* database. Possible operations are:
|
|
* * drop: to DROP all tables from the database
|
|
* * empty: to DELETE all records from each table in the database.
|
|
* * backup: Rename every table from "name" to "name_back"
|
|
* * drop_backup: DROP the backup tables.
|
|
* * rollback: DROP the new table "name" and rename "name_back" to
|
|
* "name", restoring the database to its previous state.
|
|
*
|
|
* The intent of the last two is to be able to move an existing table
|
|
* aside, query its contents with a transformation (in 2.4.x this is
|
|
* already done as the contents are loaded completely when a Qof
|
|
* session is started), save them to a new table according to a new
|
|
* database format, and finally drop the backup table; if there's an
|
|
* error during the process, rollback allows returning the table to
|
|
* its original state.
|
|
*
|
|
* @param sql_conn: The sql connection (via dbi) to which the
|
|
* transactions will be sent
|
|
* @param table_namess: StrVec of tables to operate on.
|
|
* @param op: The operation to perform.
|
|
* @return Success (TRUE) or failure.
|
|
*/
|
|
|
|
bool
|
|
GncDbiSqlConnection::table_operation(TableOpType op) noexcept
|
|
{
|
|
auto backup_tables = m_provider->get_table_list(m_conn, "%_back");
|
|
auto all_tables = m_provider->get_table_list(m_conn, "");
|
|
/* No operations on the lock table */
|
|
auto new_end = std::remove(all_tables.begin(), all_tables.end(), lock_table);
|
|
all_tables.erase(new_end, all_tables.end());
|
|
StrVec data_tables;
|
|
data_tables.reserve(all_tables.size() - backup_tables.size());
|
|
std::set_difference(all_tables.begin(), all_tables.end(),
|
|
backup_tables.begin(), backup_tables.end(),
|
|
std::back_inserter(data_tables));
|
|
switch(op)
|
|
{
|
|
case backup:
|
|
if (!backup_tables.empty())
|
|
{
|
|
PERR("Unable to backup database, an existing backup is present.");
|
|
qof_backend_set_error(m_qbe, ERR_BACKEND_DATA_CORRUPT);
|
|
return false;
|
|
}
|
|
for (auto table : data_tables)
|
|
if (!rename_table(table, table +"_back"))
|
|
return false; /* Error, trigger rollback. */
|
|
break;
|
|
case drop_backup:
|
|
for (auto table : backup_tables)
|
|
{
|
|
auto data_table = table.substr(0, table.find("_back"));
|
|
if (std::find(data_tables.begin(), data_tables.end(),
|
|
data_table) != data_tables.end())
|
|
drop_table(table); /* Other table exists, OK. */
|
|
else /* No data table, restore the backup */
|
|
rename_table(table, data_table);
|
|
}
|
|
break;
|
|
case rollback:
|
|
for (auto table : backup_tables)
|
|
{
|
|
auto data_table = table.substr(0, table.find("_back"));
|
|
if (std::find(data_tables.begin(), data_tables.end(),
|
|
data_table) != data_tables.end())
|
|
drop_table(data_table); /* Other table exists, OK. */
|
|
rename_table(table, data_table);
|
|
}
|
|
break;
|
|
case recover:
|
|
for (auto table : backup_tables)
|
|
{
|
|
auto data_table = table.substr(0, table.find("_back"));
|
|
if (std::find(data_tables.begin(), data_tables.end(),
|
|
data_table) != data_tables.end())
|
|
{
|
|
if (!merge_tables(data_table, table))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!rename_table(table, data_table))
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
GncDbiSqlConnection::drop_indexes() noexcept
|
|
{
|
|
auto index_list = m_provider->get_index_list (m_conn);
|
|
for (auto index : index_list)
|
|
{
|
|
const char* errmsg;
|
|
m_provider->drop_index (m_conn, index);
|
|
if (DBI_ERROR_NONE != dbi_conn_error (m_conn, &errmsg))
|
|
{
|
|
PERR("Failed to drop indexes %s", errmsg);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::string
|
|
GncDbiSqlConnection::add_columns_ddl(const std::string& table_name,
|
|
const ColVec& info_vec) const noexcept
|
|
{
|
|
std::string ddl;
|
|
|
|
ddl += "ALTER TABLE " + table_name;
|
|
for (auto const& info : info_vec)
|
|
{
|
|
if (info != *info_vec.begin())
|
|
{
|
|
ddl += ", ";
|
|
}
|
|
ddl += "ADD COLUMN ";
|
|
m_provider->append_col_def (ddl, info);
|
|
}
|
|
return ddl;
|
|
}
|