GnuCash c935c2f+
Loading...
Searching...
No Matches
gnc-dbisqlconnection.cpp
1/********************************************************************
2 * gnc-dbisqlconnection.cpp: Encapsulate libdbi dbi_conn *
3 * *
4 * Copyright 2016 John Ralls <jralls@ceridwen.us> *
5 * *
6 * This program is free software; you can redistribute it and/or *
7 * modify it under the terms of the GNU General Public License as *
8 * published by the Free Software Foundation; either version 2 of *
9 * the License, or (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License*
17 * along with this program; if not, contact: *
18 * *
19 * Free Software Foundation Voice: +1-617-542-5942 *
20 * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
21 * Boston, MA 02110-1301, USA gnu@gnu.org *
22\********************************************************************/
23
24#include <guid.hpp>
25#include <config.h>
26#include <platform.h>
27#include <gnc-locale-utils.h>
28
29#include <string>
30#include <regex>
31#include <sstream>
32
33#include "gnc-dbisqlconnection.hpp"
34
35static QofLogModule log_module = G_LOG_DOMAIN;
36// gnc-dbiproviderimpl.hpp has templates that need log_module defined.
37#include "gnc-dbiproviderimpl.hpp"
38
39static const unsigned int DBI_MAX_CONN_ATTEMPTS = 5;
40const std::string lock_table = "gnclock";
41
42/* --------------------------------------------------------- */
44{
45public:
46 GncDbiSqlStatement(const std::string& sql) :
47 m_sql {sql} {}
49 const char* to_sql() const override;
50 void add_where_cond(QofIdTypeConst, const PairVec&) override;
51
52private:
53 std::string m_sql;
54};
55
56
57const char*
58GncDbiSqlStatement::to_sql() const
59{
60 return m_sql.c_str();
61}
62
63void
64GncDbiSqlStatement::add_where_cond(QofIdTypeConst type_name,
65 const PairVec& col_values)
66{
67 m_sql += " WHERE ";
68 for (auto colpair : col_values)
69 {
70 if (colpair != *col_values.begin())
71 m_sql += " AND ";
72 if (colpair.second == "NULL")
73 m_sql += colpair.first + " IS " + colpair.second;
74 else
75 m_sql += colpair.first + " = " + colpair.second;
76 }
77}
78
79GncDbiSqlConnection::GncDbiSqlConnection (DbType type, QofBackend* qbe,
80 dbi_conn conn, SessionOpenMode mode) :
81 m_qbe{qbe}, m_conn{conn},
82 m_provider{type == DbType::DBI_SQLITE ?
83 make_dbi_provider<DbType::DBI_SQLITE>() :
84 type == DbType::DBI_MYSQL ?
85 make_dbi_provider<DbType::DBI_MYSQL>() :
86 make_dbi_provider<DbType::DBI_PGSQL>()},
87 m_conn_ok{true}, m_last_error{ERR_BACKEND_NO_ERR}, m_error_repeat{0},
88 m_retry{false}, m_sql_savepoint{0}, m_readonly{false}
89{
90 if (mode == SESSION_READ_ONLY)
91 m_readonly = true;
92 else if (!lock_database(mode == SESSION_BREAK_LOCK))
93 throw std::runtime_error("Failed to lock database!");
94 if (!check_and_rollback_failed_save())
95 {
96 unlock_database();
97 throw std::runtime_error("A failed safe-save was detected and rolling it back failed.");
98 }
99}
100
101bool
102GncDbiSqlConnection::lock_database (bool break_lock)
103{
104 const char *errstr;
105 /* Protect everything with a single transaction to prevent races */
106 if (!begin_transaction())
107 return false;
108 auto tables = m_provider->get_table_list(m_conn, lock_table);
109 if (tables.empty())
110 {
111 auto result = dbi_conn_queryf (m_conn,
112 "CREATE TABLE %s ( Hostname varchar(%d), PID int )",
113 lock_table.c_str(),
114 GNC_HOST_NAME_MAX);
115 if (result)
116 {
117 dbi_result_free (result);
118 result = nullptr;
119 }
120 if (dbi_conn_error (m_conn, &errstr))
121 {
122 PERR ("Error %s creating lock table", errstr);
124 return false;
125 }
126 }
127
128 /* Check for an existing entry; delete it if break_lock is true, otherwise fail */
129 char hostname[ GNC_HOST_NAME_MAX + 1 ];
130 auto result = dbi_conn_queryf (m_conn, "SELECT * FROM %s",
131 lock_table.c_str());
132 if (result && dbi_result_get_numrows (result))
133 {
134 dbi_result_free (result);
135 result = nullptr;
136 if (!break_lock)
137 {
138 qof_backend_set_error (m_qbe, ERR_BACKEND_LOCKED);
139 /* FIXME: After enhancing the qof_backend_error mechanism, report in the dialog what is the hostname of the machine holding the lock. */
141 return false;
142 }
143 result = dbi_conn_queryf (m_conn, "DELETE FROM %s", lock_table.c_str());
144 if (!result)
145 {
147 m_qbe->set_message("Failed to delete lock record");
149 return false;
150 }
151 dbi_result_free (result);
152 result = nullptr;
153 }
154 /* Add an entry and commit the transaction */
155 memset (hostname, 0, sizeof (hostname));
156 gethostname (hostname, GNC_HOST_NAME_MAX);
157 result = dbi_conn_queryf (m_conn,
158 "INSERT INTO %s VALUES ('%s', '%d')",
159 lock_table.c_str(), hostname, (int)GETPID ());
160 if (!result)
161 {
163 m_qbe->set_message("Failed to create lock record");
165 return false;
166 }
167 dbi_result_free (result);
168 return commit_transaction();
169}
170
171void
172GncDbiSqlConnection::unlock_database ()
173{
174 if (m_conn == nullptr) return;
175 if (m_readonly) return;
176 auto dbi_error{dbi_conn_error (m_conn, nullptr)};
177 g_return_if_fail (dbi_error == DBI_ERROR_NONE || dbi_error == DBI_ERROR_BADIDX);
178
179 auto tables = m_provider->get_table_list (m_conn, lock_table);
180 if (tables.empty())
181 {
182 PWARN ("No lock table in database, so not unlocking it.");
183 return;
184 }
185 if (begin_transaction())
186 {
187 /* Delete the entry if it's our hostname and PID */
188 char hostname[ GNC_HOST_NAME_MAX + 1 ];
189
190 memset (hostname, 0, sizeof (hostname));
191 gethostname (hostname, GNC_HOST_NAME_MAX);
192 auto result = dbi_conn_queryf (m_conn,
193 "SELECT * FROM %s WHERE Hostname = '%s' "
194 "AND PID = '%d'", lock_table.c_str(),
195 hostname,
196 (int)GETPID ());
197 if (result && dbi_result_get_numrows (result))
198 {
199 if (result)
200 {
201 dbi_result_free (result);
202 result = nullptr;
203 }
204 result = dbi_conn_queryf (m_conn, "DELETE FROM %s",
205 lock_table.c_str());
206 if (!result)
207 {
208 PERR ("Failed to delete the lock entry");
211 return;
212 }
213 else
214 {
215 dbi_result_free (result);
216 result = nullptr;
217 }
219 return;
220 }
222 PWARN ("There was no lock entry in the Lock table");
223 return;
224 }
225 PWARN ("Unable to get a lock on LOCK, so failed to clear the lock entry.");
227}
228
229bool
230GncDbiSqlConnection::check_and_rollback_failed_save()
231{
232 auto backup_tables = m_provider->get_table_list(m_conn, "%back");
233 if (backup_tables.empty())
234 return true;
235 auto merge_tables = m_provider->get_table_list(m_conn, "%_merge");
236 if (!merge_tables.empty())
237 {
238 PERR("Merge tables exist in the database indicating a previous"
239 "attempt to recover from a failed safe-save. Automatic"
240 "recovery is beyond GnuCash's ability, you must recover"
241 "by hand or restore from a good backup.");
242 return false;
243 }
244 return table_operation(recover);
245}
246
247GncDbiSqlConnection::~GncDbiSqlConnection()
248{
249 if (m_conn)
250 {
251 unlock_database();
252 dbi_conn_close(m_conn);
253 m_conn = nullptr;
254 }
255}
256
258GncDbiSqlConnection::execute_select_statement (const GncSqlStatementPtr& stmt)
259 noexcept
260{
261 dbi_result result;
262
263 DEBUG ("SQL: %s\n", stmt->to_sql());
264 auto locale = gnc_push_locale (LC_NUMERIC, "C");
265 do
266 {
267 init_error ();
268 result = dbi_conn_query (m_conn, stmt->to_sql());
269 }
270 while (m_retry);
271 if (result == nullptr)
272 {
273 PERR ("Error executing SQL %s\n", stmt->to_sql());
274 if(m_last_error)
275 m_qbe->set_error(m_last_error);
276 else
277 m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
278 }
279 gnc_pop_locale (LC_NUMERIC, locale);
280 return GncSqlResultPtr(new GncDbiSqlResult (this, result));
281}
282
283int
285 noexcept
286{
287 dbi_result result;
288
289 DEBUG ("SQL: %s\n", stmt->to_sql());
290 do
291 {
292 init_error ();
293 result = dbi_conn_query (m_conn, stmt->to_sql());
294 }
295 while (m_retry);
296 if (result == nullptr && m_last_error)
297 {
298 PERR ("Error executing SQL %s\n", stmt->to_sql());
299 if(m_last_error)
300 m_qbe->set_error(m_last_error);
301 else
302 m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
303 return -1;
304 }
305 if (!result)
306 return 0;
307 auto num_rows = (gint)dbi_result_get_numrows_affected (result);
308 auto status = dbi_result_free (result);
309 if (status < 0)
310 {
311 PERR ("Error in dbi_result_free() result\n");
312 if(m_last_error)
313 m_qbe->set_error(m_last_error);
314 else
315 m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
316 }
317 return num_rows;
318}
319
320GncSqlStatementPtr
321GncDbiSqlConnection::create_statement_from_sql (const std::string& sql)
322 const noexcept
323{
324 return std::unique_ptr<GncSqlStatement>{new GncDbiSqlStatement (sql)};
325}
326
327bool
328GncDbiSqlConnection::does_table_exist (const std::string& table_name)
329 const noexcept
330{
331 return ! m_provider->get_table_list(m_conn, table_name).empty();
332}
333
334bool
336{
337 dbi_result result;
338
339 DEBUG ("BEGIN\n");
340
341 if (!verify ())
342 {
343 PERR ("gnc_dbi_verify_conn() failed\n");
345 return false;
346 }
347
348 do
349 {
350 init_error ();
351 if (m_sql_savepoint == 0)
352 result = dbi_conn_queryf (m_conn, "BEGIN");
353 else
354 {
355 std::ostringstream savepoint;
356 savepoint << "savepoint_" << m_sql_savepoint;
357 result = dbi_conn_queryf(m_conn, "SAVEPOINT %s",
358 savepoint.str().c_str());
359 }
360 }
361 while (m_retry);
362
363 if (!result)
364 {
365 PERR ("BEGIN transaction failed()\n");
367 return false;
368 }
369 if (dbi_result_free (result) < 0)
370 {
371 PERR ("Error in dbi_result_free() result\n");
373 return false;
374 }
375 ++m_sql_savepoint;
376 return true;
377}
378
379bool
381{
382 DEBUG ("ROLLBACK\n");
383 if (m_sql_savepoint == 0) return false;
384 dbi_result result;
385 if (m_sql_savepoint == 1)
386 result = dbi_conn_query (m_conn, "ROLLBACK");
387 else
388 {
389 std::ostringstream savepoint;
390 savepoint << "savepoint_" << m_sql_savepoint - 1;
391 result = dbi_conn_queryf(m_conn, "ROLLBACK TO SAVEPOINT %s",
392 savepoint.str().c_str());
393 }
394 if (!result)
395 {
396 PERR ("Error in conn_rollback_transaction()\n");
398 return false;
399 }
400
401 if (dbi_result_free (result) < 0)
402 {
403 PERR ("Error in dbi_result_free() result\n");
405 return false;
406 }
407
408 --m_sql_savepoint;
409 return true;
410}
411
412bool
414{
415 DEBUG ("COMMIT\n");
416 if (m_sql_savepoint == 0) return false;
417 dbi_result result;
418 if (m_sql_savepoint == 1)
419 result = dbi_conn_queryf (m_conn, "COMMIT");
420 else
421 {
422 std::ostringstream savepoint;
423 savepoint << "savepoint_" << m_sql_savepoint - 1;
424 result = dbi_conn_queryf(m_conn, "RELEASE SAVEPOINT %s",
425 savepoint.str().c_str());
426 }
427
428 if (!result)
429 {
430 PERR ("Error in conn_commit_transaction()\n");
432 return false;
433 }
434
435 if (dbi_result_free (result) < 0)
436 {
437 PERR ("Error in dbi_result_free() result\n");
439 return false;
440 }
441 --m_sql_savepoint;
442 return true;
443}
444
445
446bool
447GncDbiSqlConnection::create_table (const std::string& table_name,
448 const ColVec& info_vec) const noexcept
449{
450 std::string ddl;
451 unsigned int col_num = 0;
452
453 ddl += "CREATE TABLE " + table_name + "(";
454 for (auto const& info : info_vec)
455 {
456 if (col_num++ != 0)
457 {
458 ddl += ", ";
459 }
460 m_provider->append_col_def (ddl, info);
461 }
462 ddl += ")";
463
464 if (ddl.empty())
465 return false;
466
467 DEBUG ("SQL: %s\n", ddl.c_str());
468 auto result = dbi_conn_query (m_conn, ddl.c_str());
469 auto status = dbi_result_free (result);
470 if (status < 0)
471 {
472 PERR ("Error in dbi_result_free() result\n");
474 }
475
476 return true;
477}
478
479static std::string
480create_index_ddl (const GncSqlConnection* conn, const std::string& index_name,
481 const std::string& table_name, const EntryVec& col_table)
482{
483 std::string ddl;
484 ddl += "CREATE INDEX " + index_name + " ON " + table_name + "(";
485 for (const auto& table_row : col_table)
486 {
487 if (table_row != *col_table.begin())
488 {
489 ddl =+ ", ";
490 }
491 ddl += table_row->name();
492 }
493 ddl += ")";
494 return ddl;
495}
496
497bool
498GncDbiSqlConnection::create_index(const std::string& index_name,
499 const std::string& table_name,
500 const EntryVec& col_table) const noexcept
501{
502 auto ddl = create_index_ddl (this, index_name, table_name, col_table);
503 if (ddl.empty())
504 return false;
505 DEBUG ("SQL: %s\n", ddl.c_str());
506 auto result = dbi_conn_query (m_conn, ddl.c_str());
507 auto status = dbi_result_free (result);
508 if (status < 0)
509 {
510 PERR ("Error in dbi_result_free() result\n");
512 }
513
514 return true;
515}
516
517bool
518GncDbiSqlConnection::add_columns_to_table(const std::string& table_name,
519 const ColVec& info_vec)
520 const noexcept
521{
522 auto ddl = add_columns_ddl(table_name, info_vec);
523 if (ddl.empty())
524 return false;
525
526 DEBUG ("SQL: %s\n", ddl.c_str());
527 auto result = dbi_conn_query (m_conn, ddl.c_str());
528 auto status = dbi_result_free (result);
529 if (status < 0)
530 {
531 PERR( "Error in dbi_result_free() result\n" );
533 }
534
535 return true;
536}
537
538std::string
539GncDbiSqlConnection::quote_string (const std::string& unquoted_str)
540 const noexcept
541{
542 char* quoted_str;
543
544 dbi_conn_quote_string_copy (m_conn, unquoted_str.c_str(),
545 &quoted_str);
546 if (quoted_str == nullptr)
547 return std::string{""};
548 std::string retval{quoted_str};
549 free(quoted_str);
550 return retval;
551}
552
553
557bool
559{
560 if (m_conn_ok)
561 return true;
562
563 /* We attempt to connect only once here. The error function will
564 * automatically re-attempt up until DBI_MAX_CONN_ATTEMPTS time to connect
565 * if this call fails. After all these attempts, conn_ok will indicate if
566 * there is a valid connection or not.
567 */
568 init_error ();
569 m_conn_ok = true;
570 (void)dbi_conn_connect (m_conn);
571
572 return m_conn_ok;
573}
574
575bool
576GncDbiSqlConnection::retry_connection(const char* msg)
577 noexcept
578{
579 while (m_retry && m_error_repeat <= DBI_MAX_CONN_ATTEMPTS)
580 {
581 m_conn_ok = false;
582 if (dbi_conn_connect(m_conn) == 0)
583 {
584 init_error();
585 m_conn_ok = true;
586 return true;
587 }
588#ifdef G_OS_WIN32
589 const guint backoff_msecs = 1;
590 Sleep (backoff_msecs * 2 << ++m_error_repeat);
591#else
592 const guint backoff_usecs = 1000;
593 usleep (backoff_usecs * 2 << ++m_error_repeat);
594#endif
595 PINFO ("DBI error: %s - Reconnecting...\n", msg);
596
597 }
598 PERR ("DBI error: %s - Giving up after %d consecutive attempts.\n", msg,
599 DBI_MAX_CONN_ATTEMPTS);
600 m_conn_ok = false;
601 return false;
602}
603
604bool
605GncDbiSqlConnection::rename_table(const std::string& old_name,
606 const std::string& new_name)
607{
608 std::string sql = "ALTER TABLE " + old_name + " RENAME TO " + new_name;
609 auto stmt = create_statement_from_sql(sql);
610 return execute_nonselect_statement(stmt) >= 0;
611}
612
613bool
614GncDbiSqlConnection::drop_table(const std::string& table)
615{
616 std::string sql = "DROP TABLE " + table;
617 auto stmt = create_statement_from_sql(sql);
618 return execute_nonselect_statement(stmt) >= 0;
619}
620
621bool
622GncDbiSqlConnection::merge_tables(const std::string& table,
623 const std::string& other)
624{
625 auto merge_table = table + "_merge";
626 std::string sql = "CREATE TABLE " + merge_table + " AS SELECT * FROM " +
627 table + " UNION SELECT * FROM " + other;
628 auto stmt = create_statement_from_sql(sql);
629 if (execute_nonselect_statement(stmt) < 0)
630 return false;
631 sql = std::string("DELETE FROM ") + table;
632 stmt = create_statement_from_sql(sql);
633 if (execute_nonselect_statement(stmt) < 0)
634 return false;
635 sql = std::string("INSERT INTO ") + table +
636 " SELECT * FROM " + merge_table;
637 stmt = create_statement_from_sql(sql);
638 if (execute_nonselect_statement(stmt) < 0)
639 return false;
640 if (!drop_table(merge_table))
641 return false;
642 return drop_table(other);
643}
644
670bool
672{
673 auto backup_tables = m_provider->get_table_list(m_conn, "%_back");
674 auto all_tables = m_provider->get_table_list(m_conn, "");
675 /* No operations on the lock table */
676 auto new_end = std::remove(all_tables.begin(), all_tables.end(), lock_table);
677 all_tables.erase(new_end, all_tables.end());
678 StrVec data_tables;
679 data_tables.reserve(all_tables.size() - backup_tables.size());
680 std::set_difference(all_tables.begin(), all_tables.end(),
681 backup_tables.begin(), backup_tables.end(),
682 std::back_inserter(data_tables));
683 switch(op)
684 {
685 case backup:
686 if (!backup_tables.empty())
687 {
688 PERR("Unable to backup database, an existing backup is present.");
690 return false;
691 }
692 for (auto table : data_tables)
693 if (!rename_table(table, table +"_back"))
694 return false; /* Error, trigger rollback. */
695 break;
696 case drop_backup:
697 for (auto table : backup_tables)
698 {
699 auto data_table = table.substr(0, table.find("_back"));
700 if (std::find(data_tables.begin(), data_tables.end(),
701 data_table) != data_tables.end())
702 drop_table(table); /* Other table exists, OK. */
703 else /* No data table, restore the backup */
704 rename_table(table, data_table);
705 }
706 break;
707 case rollback:
708 for (auto table : backup_tables)
709 {
710 auto data_table = table.substr(0, table.find("_back"));
711 if (std::find(data_tables.begin(), data_tables.end(),
712 data_table) != data_tables.end())
713 drop_table(data_table); /* Other table exists, OK. */
714 rename_table(table, data_table);
715 }
716 break;
717 case recover:
718 for (auto table : backup_tables)
719 {
720 auto data_table = table.substr(0, table.find("_back"));
721 if (std::find(data_tables.begin(), data_tables.end(),
722 data_table) != data_tables.end())
723 {
724 if (!merge_tables(data_table, table))
725 return false;
726 }
727 else
728 {
729 if (!rename_table(table, data_table))
730 return false;
731 }
732 }
733 break;
734 }
735 return true;
736}
737
738bool
739GncDbiSqlConnection::drop_indexes() noexcept
740{
741 auto index_list = m_provider->get_index_list (m_conn);
742 for (auto index : index_list)
743 {
744 const char* errmsg;
745 m_provider->drop_index (m_conn, index);
746 if (DBI_ERROR_NONE != dbi_conn_error (m_conn, &errmsg))
747 {
748 PERR("Failed to drop indexes %s", errmsg);
749 return false;
750 }
751 }
752 return true;
753}
754
755std::string
756GncDbiSqlConnection::add_columns_ddl(const std::string& table_name,
757 const ColVec& info_vec) const noexcept
758{
759 std::string ddl;
760
761 ddl += "ALTER TABLE " + table_name;
762 for (auto const& info : info_vec)
763 {
764 if (info != *info_vec.begin())
765 {
766 ddl += ", ";
767 }
768 ddl += "ADD COLUMN ";
769 m_provider->append_col_def (ddl, info);
770 }
771 return ddl;
772}
bool table_operation(TableOpType op) noexcept
Perform a specified SQL operation on every table in a database.
bool commit_transaction() noexcept override
Returns TRUE if successful, FALSE if error.
bool add_columns_to_table(const std::string &, const ColVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
int execute_nonselect_statement(const GncSqlStatementPtr &) noexcept override
Returns false if error.
bool create_index(const std::string &, const std::string &, const EntryVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
bool does_table_exist(const std::string &) const noexcept override
Returns true if successful.
bool begin_transaction() noexcept override
Returns TRUE if successful, false if error.
bool verify() noexcept override
Check if the dbi connection is valid.
bool rollback_transaction() noexcept override
Returns TRUE if successful, FALSE if error.
bool create_table(const std::string &, const ColVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
An iterable wrapper for dbi_result; allows using C++11 range for.
Encapsulate the connection to the database.
Pure virtual class to iterate over a query result set.
SQL statement provider.
void qof_backend_set_error(QofBackend *qof_be, QofBackendError err)
Set the error on the specified QofBackend.
SessionOpenMode
Mode for opening sessions.
Definition qofsession.h:121
@ ERR_BACKEND_SERVER_ERR
error in response from server
Definition qofbackend.h:71
@ ERR_BACKEND_DATA_CORRUPT
data in db is corrupt
Definition qofbackend.h:70
@ SESSION_READ_ONLY
Create a new store at the URI even if a store already exists there.
Definition qofsession.h:128
@ SESSION_BREAK_LOCK
Open the session read-only, ignoring any existing lock and not creating one if the URI isn't locked.
Definition qofsession.h:130
const gchar * QofIdTypeConst
QofIdTypeConst declaration.
Definition qofid.h:82
#define PINFO(format, args...)
Print an informational note.
Definition qoflog.h:256
#define DEBUG(format, args...)
Print a debugging message.
Definition qoflog.h:264
#define PERR(format, args...)
Log a serious error.
Definition qoflog.h:244
#define PWARN(format, args...)
Log a warning.
Definition qoflog.h:250
void set_message(std::string &&)
Set a descriptive message that can be displayed to the user when there's an error.
void set_error(QofBackendError err)
Set the error value only if there isn't already an error already.