GnuCash  5.6-150-g038405b370+
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 
35 static QofLogModule log_module = G_LOG_DOMAIN;
36 // gnc-dbiproviderimpl.hpp has templates that need log_module defined.
37 #include "gnc-dbiproviderimpl.hpp"
38 
39 static const unsigned int DBI_MAX_CONN_ATTEMPTS = 5;
40 const std::string lock_table = "gnclock";
41 
42 /* --------------------------------------------------------- */
44 {
45 public:
46  GncDbiSqlStatement(const std::string& sql) :
47  m_sql {sql} {}
48  ~GncDbiSqlStatement() {}
49  const char* to_sql() const override;
50  void add_where_cond(QofIdTypeConst, const PairVec&) override;
51 
52 private:
53  std::string m_sql;
54 };
55 
56 
57 const char*
58 GncDbiSqlStatement::to_sql() const
59 {
60  return m_sql.c_str();
61 }
62 
63 void
64 GncDbiSqlStatement::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 
79 GncDbiSqlConnection::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 
101 bool
102 GncDbiSqlConnection::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  {
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 
171 void
172 GncDbiSqlConnection::unlock_database ()
173 {
174  if (m_conn == nullptr) return;
175  if (m_readonly) return;
176  g_return_if_fail (dbi_conn_error (m_conn, nullptr) == 0);
177 
178  auto tables = m_provider->get_table_list (m_conn, lock_table);
179  if (tables.empty())
180  {
181  PWARN ("No lock table in database, so not unlocking it.");
182  return;
183  }
184  if (begin_transaction())
185  {
186  /* Delete the entry if it's our hostname and PID */
187  char hostname[ GNC_HOST_NAME_MAX + 1 ];
188 
189  memset (hostname, 0, sizeof (hostname));
190  gethostname (hostname, GNC_HOST_NAME_MAX);
191  auto result = dbi_conn_queryf (m_conn,
192  "SELECT * FROM %s WHERE Hostname = '%s' "
193  "AND PID = '%d'", lock_table.c_str(),
194  hostname,
195  (int)GETPID ());
196  if (result && dbi_result_get_numrows (result))
197  {
198  if (result)
199  {
200  dbi_result_free (result);
201  result = nullptr;
202  }
203  result = dbi_conn_queryf (m_conn, "DELETE FROM %s",
204  lock_table.c_str());
205  if (!result)
206  {
207  PERR ("Failed to delete the lock entry");
210  return;
211  }
212  else
213  {
214  dbi_result_free (result);
215  result = nullptr;
216  }
218  return;
219  }
221  PWARN ("There was no lock entry in the Lock table");
222  return;
223  }
224  PWARN ("Unable to get a lock on LOCK, so failed to clear the lock entry.");
226 }
227 
228 bool
229 GncDbiSqlConnection::check_and_rollback_failed_save()
230 {
231  auto backup_tables = m_provider->get_table_list(m_conn, "%back");
232  if (backup_tables.empty())
233  return true;
234  auto merge_tables = m_provider->get_table_list(m_conn, "%_merge");
235  if (!merge_tables.empty())
236  {
237  PERR("Merge tables exist in the database indicating a previous"
238  "attempt to recover from a failed safe-save. Automatic"
239  "recovery is beyond GnuCash's ability, you must recover"
240  "by hand or restore from a good backup.");
241  return false;
242  }
243  return table_operation(recover);
244 }
245 
246 GncDbiSqlConnection::~GncDbiSqlConnection()
247 {
248  if (m_conn)
249  {
250  unlock_database();
251  dbi_conn_close(m_conn);
252  m_conn = nullptr;
253  }
254 }
255 
257 GncDbiSqlConnection::execute_select_statement (const GncSqlStatementPtr& stmt)
258  noexcept
259 {
260  dbi_result result;
261 
262  DEBUG ("SQL: %s\n", stmt->to_sql());
263  auto locale = gnc_push_locale (LC_NUMERIC, "C");
264  do
265  {
266  init_error ();
267  result = dbi_conn_query (m_conn, stmt->to_sql());
268  }
269  while (m_retry);
270  if (result == nullptr)
271  {
272  PERR ("Error executing SQL %s\n", stmt->to_sql());
273  if(m_last_error)
274  m_qbe->set_error(m_last_error);
275  else
276  m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
277  }
278  gnc_pop_locale (LC_NUMERIC, locale);
279  return GncSqlResultPtr(new GncDbiSqlResult (this, result));
280 }
281 
282 int
283 GncDbiSqlConnection::execute_nonselect_statement (const GncSqlStatementPtr& stmt)
284  noexcept
285 {
286  dbi_result result;
287 
288  DEBUG ("SQL: %s\n", stmt->to_sql());
289  do
290  {
291  init_error ();
292  result = dbi_conn_query (m_conn, stmt->to_sql());
293  }
294  while (m_retry);
295  if (result == nullptr && m_last_error)
296  {
297  PERR ("Error executing SQL %s\n", stmt->to_sql());
298  if(m_last_error)
299  m_qbe->set_error(m_last_error);
300  else
301  m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
302  return -1;
303  }
304  if (!result)
305  return 0;
306  auto num_rows = (gint)dbi_result_get_numrows_affected (result);
307  auto status = dbi_result_free (result);
308  if (status < 0)
309  {
310  PERR ("Error in dbi_result_free() result\n");
311  if(m_last_error)
312  m_qbe->set_error(m_last_error);
313  else
314  m_qbe->set_error(ERR_BACKEND_SERVER_ERR);
315  }
316  return num_rows;
317 }
318 
319 GncSqlStatementPtr
320 GncDbiSqlConnection::create_statement_from_sql (const std::string& sql)
321  const noexcept
322 {
323  return std::unique_ptr<GncSqlStatement>{new GncDbiSqlStatement (sql)};
324 }
325 
326 bool
327 GncDbiSqlConnection::does_table_exist (const std::string& table_name)
328  const noexcept
329 {
330  return ! m_provider->get_table_list(m_conn, table_name).empty();
331 }
332 
333 bool
335 {
336  dbi_result result;
337 
338  DEBUG ("BEGIN\n");
339 
340  if (!verify ())
341  {
342  PERR ("gnc_dbi_verify_conn() failed\n");
344  return false;
345  }
346 
347  do
348  {
349  init_error ();
350  if (m_sql_savepoint == 0)
351  result = dbi_conn_queryf (m_conn, "BEGIN");
352  else
353  {
354  std::ostringstream savepoint;
355  savepoint << "savepoint_" << m_sql_savepoint;
356  result = dbi_conn_queryf(m_conn, "SAVEPOINT %s",
357  savepoint.str().c_str());
358  }
359  }
360  while (m_retry);
361 
362  if (!result)
363  {
364  PERR ("BEGIN transaction failed()\n");
366  return false;
367  }
368  if (dbi_result_free (result) < 0)
369  {
370  PERR ("Error in dbi_result_free() result\n");
372  return false;
373  }
374  ++m_sql_savepoint;
375  return true;
376 }
377 
378 bool
380 {
381  DEBUG ("ROLLBACK\n");
382  if (m_sql_savepoint == 0) return false;
383  dbi_result result;
384  if (m_sql_savepoint == 1)
385  result = dbi_conn_query (m_conn, "ROLLBACK");
386  else
387  {
388  std::ostringstream savepoint;
389  savepoint << "savepoint_" << m_sql_savepoint - 1;
390  result = dbi_conn_queryf(m_conn, "ROLLBACK TO SAVEPOINT %s",
391  savepoint.str().c_str());
392  }
393  if (!result)
394  {
395  PERR ("Error in conn_rollback_transaction()\n");
397  return false;
398  }
399 
400  if (dbi_result_free (result) < 0)
401  {
402  PERR ("Error in dbi_result_free() result\n");
404  return false;
405  }
406 
407  --m_sql_savepoint;
408  return true;
409 }
410 
411 bool
413 {
414  DEBUG ("COMMIT\n");
415  if (m_sql_savepoint == 0) return false;
416  dbi_result result;
417  if (m_sql_savepoint == 1)
418  result = dbi_conn_queryf (m_conn, "COMMIT");
419  else
420  {
421  std::ostringstream savepoint;
422  savepoint << "savepoint_" << m_sql_savepoint - 1;
423  result = dbi_conn_queryf(m_conn, "RELEASE SAVEPOINT %s",
424  savepoint.str().c_str());
425  }
426 
427  if (!result)
428  {
429  PERR ("Error in conn_commit_transaction()\n");
431  return false;
432  }
433 
434  if (dbi_result_free (result) < 0)
435  {
436  PERR ("Error in dbi_result_free() result\n");
438  return false;
439  }
440  --m_sql_savepoint;
441  return true;
442 }
443 
444 
445 bool
446 GncDbiSqlConnection::create_table (const std::string& table_name,
447  const ColVec& info_vec) const noexcept
448 {
449  std::string ddl;
450  unsigned int col_num = 0;
451 
452  ddl += "CREATE TABLE " + table_name + "(";
453  for (auto const& info : info_vec)
454  {
455  if (col_num++ != 0)
456  {
457  ddl += ", ";
458  }
459  m_provider->append_col_def (ddl, info);
460  }
461  ddl += ")";
462 
463  if (ddl.empty())
464  return false;
465 
466  DEBUG ("SQL: %s\n", ddl.c_str());
467  auto result = dbi_conn_query (m_conn, ddl.c_str());
468  auto status = dbi_result_free (result);
469  if (status < 0)
470  {
471  PERR ("Error in dbi_result_free() result\n");
473  }
474 
475  return true;
476 }
477 
478 static std::string
479 create_index_ddl (const GncSqlConnection* conn, const std::string& index_name,
480  const std::string& table_name, const EntryVec& col_table)
481 {
482  std::string ddl;
483  ddl += "CREATE INDEX " + index_name + " ON " + table_name + "(";
484  for (const auto& table_row : col_table)
485  {
486  if (table_row != *col_table.begin())
487  {
488  ddl =+ ", ";
489  }
490  ddl += table_row->name();
491  }
492  ddl += ")";
493  return ddl;
494 }
495 
496 bool
497 GncDbiSqlConnection::create_index(const std::string& index_name,
498  const std::string& table_name,
499  const EntryVec& col_table) const noexcept
500 {
501  auto ddl = create_index_ddl (this, index_name, table_name, col_table);
502  if (ddl.empty())
503  return false;
504  DEBUG ("SQL: %s\n", ddl.c_str());
505  auto result = dbi_conn_query (m_conn, ddl.c_str());
506  auto status = dbi_result_free (result);
507  if (status < 0)
508  {
509  PERR ("Error in dbi_result_free() result\n");
511  }
512 
513  return true;
514 }
515 
516 bool
517 GncDbiSqlConnection::add_columns_to_table(const std::string& table_name,
518  const ColVec& info_vec)
519  const noexcept
520 {
521  auto ddl = add_columns_ddl(table_name, info_vec);
522  if (ddl.empty())
523  return false;
524 
525  DEBUG ("SQL: %s\n", ddl.c_str());
526  auto result = dbi_conn_query (m_conn, ddl.c_str());
527  auto status = dbi_result_free (result);
528  if (status < 0)
529  {
530  PERR( "Error in dbi_result_free() result\n" );
532  }
533 
534  return true;
535 }
536 
537 std::string
538 GncDbiSqlConnection::quote_string (const std::string& unquoted_str)
539  const noexcept
540 {
541  char* quoted_str;
542 
543  dbi_conn_quote_string_copy (m_conn, unquoted_str.c_str(),
544  &quoted_str);
545  if (quoted_str == nullptr)
546  return std::string{""};
547  std::string retval{quoted_str};
548  free(quoted_str);
549  return retval;
550 }
551 
552 
556 bool
558 {
559  if (m_conn_ok)
560  return true;
561 
562  /* We attempt to connect only once here. The error function will
563  * automatically re-attempt up until DBI_MAX_CONN_ATTEMPTS time to connect
564  * if this call fails. After all these attempts, conn_ok will indicate if
565  * there is a valid connection or not.
566  */
567  init_error ();
568  m_conn_ok = true;
569  (void)dbi_conn_connect (m_conn);
570 
571  return m_conn_ok;
572 }
573 
574 bool
575 GncDbiSqlConnection::retry_connection(const char* msg)
576  noexcept
577 {
578  while (m_retry && m_error_repeat <= DBI_MAX_CONN_ATTEMPTS)
579  {
580  m_conn_ok = false;
581  if (dbi_conn_connect(m_conn) == 0)
582  {
583  init_error();
584  m_conn_ok = true;
585  return true;
586  }
587 #ifdef G_OS_WIN32
588  const guint backoff_msecs = 1;
589  Sleep (backoff_msecs * 2 << ++m_error_repeat);
590 #else
591  const guint backoff_usecs = 1000;
592  usleep (backoff_usecs * 2 << ++m_error_repeat);
593 #endif
594  PINFO ("DBI error: %s - Reconnecting...\n", msg);
595 
596  }
597  PERR ("DBI error: %s - Giving up after %d consecutive attempts.\n", msg,
598  DBI_MAX_CONN_ATTEMPTS);
599  m_conn_ok = false;
600  return false;
601 }
602 
603 bool
604 GncDbiSqlConnection::rename_table(const std::string& old_name,
605  const std::string& new_name)
606 {
607  std::string sql = "ALTER TABLE " + old_name + " RENAME TO " + new_name;
608  auto stmt = create_statement_from_sql(sql);
609  return execute_nonselect_statement(stmt) >= 0;
610 }
611 
612 bool
613 GncDbiSqlConnection::drop_table(const std::string& table)
614 {
615  std::string sql = "DROP TABLE " + table;
616  auto stmt = create_statement_from_sql(sql);
617  return execute_nonselect_statement(stmt) >= 0;
618 }
619 
620 bool
621 GncDbiSqlConnection::merge_tables(const std::string& table,
622  const std::string& other)
623 {
624  auto merge_table = table + "_merge";
625  std::string sql = "CREATE TABLE " + merge_table + " AS SELECT * FROM " +
626  table + " UNION SELECT * FROM " + other;
627  auto stmt = create_statement_from_sql(sql);
628  if (execute_nonselect_statement(stmt) < 0)
629  return false;
630  if (!drop_table(table))
631  return false;
632  if (!rename_table(merge_table, table))
633  return false;
634  return drop_table(other);
635 }
636 
662 bool
663 GncDbiSqlConnection::table_operation(TableOpType op) noexcept
664 {
665  auto backup_tables = m_provider->get_table_list(m_conn, "%_back");
666  auto all_tables = m_provider->get_table_list(m_conn, "");
667  /* No operations on the lock table */
668  auto new_end = std::remove(all_tables.begin(), all_tables.end(), lock_table);
669  all_tables.erase(new_end, all_tables.end());
670  StrVec data_tables;
671  data_tables.reserve(all_tables.size() - backup_tables.size());
672  std::set_difference(all_tables.begin(), all_tables.end(),
673  backup_tables.begin(), backup_tables.end(),
674  std::back_inserter(data_tables));
675  switch(op)
676  {
677  case backup:
678  if (!backup_tables.empty())
679  {
680  PERR("Unable to backup database, an existing backup is present.");
682  return false;
683  }
684  for (auto table : data_tables)
685  if (!rename_table(table, table +"_back"))
686  return false; /* Error, trigger rollback. */
687  break;
688  case drop_backup:
689  for (auto table : backup_tables)
690  {
691  auto data_table = table.substr(0, table.find("_back"));
692  if (std::find(data_tables.begin(), data_tables.end(),
693  data_table) != data_tables.end())
694  drop_table(table); /* Other table exists, OK. */
695  else /* No data table, restore the backup */
696  rename_table(table, data_table);
697  }
698  break;
699  case rollback:
700  for (auto table : backup_tables)
701  {
702  auto data_table = table.substr(0, table.find("_back"));
703  if (std::find(data_tables.begin(), data_tables.end(),
704  data_table) != data_tables.end())
705  drop_table(data_table); /* Other table exists, OK. */
706  rename_table(table, data_table);
707  }
708  break;
709  case recover:
710  for (auto table : backup_tables)
711  {
712  auto data_table = table.substr(0, table.find("_back"));
713  if (std::find(data_tables.begin(), data_tables.end(),
714  data_table) != data_tables.end())
715  {
716  if (!merge_tables(data_table, table))
717  return false;
718  }
719  else
720  {
721  if (!rename_table(table, data_table))
722  return false;
723  }
724  }
725  break;
726  }
727  return true;
728 }
729 
730 bool
731 GncDbiSqlConnection::drop_indexes() noexcept
732 {
733  auto index_list = m_provider->get_index_list (m_conn);
734  for (auto index : index_list)
735  {
736  const char* errmsg;
737  m_provider->drop_index (m_conn, index);
738  if (DBI_ERROR_NONE != dbi_conn_error (m_conn, &errmsg))
739  {
740  PERR("Failed to drop indexes %s", errmsg);
741  return false;
742  }
743  }
744  return true;
745 }
746 
747 std::string
748 GncDbiSqlConnection::add_columns_ddl(const std::string& table_name,
749  const ColVec& info_vec) const noexcept
750 {
751  std::string ddl;
752 
753  ddl += "ALTER TABLE " + table_name;
754  for (auto const& info : info_vec)
755  {
756  if (info != *info_vec.begin())
757  {
758  ddl += ", ";
759  }
760  ddl += "ADD COLUMN ";
761  m_provider->append_col_def (ddl, info);
762  }
763  return ddl;
764 }
bool verify() noexcept override
Check if the dbi connection is valid.
void set_message(std::string &&)
Set a descriptive message that can be displayed to the user when there&#39;s an error.
Definition: qof-backend.cpp:79
#define G_LOG_DOMAIN
Functions providing the SX List as a plugin page.
#define PINFO(format, args...)
Print an informational note.
Definition: qoflog.h:256
bool does_table_exist(const std::string &) const noexcept override
Returns true if successful.
const gchar * QofIdTypeConst
QofIdTypeConst declaration.
Definition: qofid.h:82
void qof_backend_set_error(QofBackend *qof_be, QofBackendError err)
Set the error on the specified QofBackend.
SQL statement provider.
#define DEBUG(format, args...)
Print a debugging message.
Definition: qoflog.h:264
in use by another user (ETXTBSY)
Definition: qofbackend.h:66
Open the session read-only, ignoring any existing lock and not creating one if the URI isn&#39;t locked...
Definition: qofsession.h:130
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
Create a new store at the URI even if a store already exists there.
Definition: qofsession.h:128
#define PWARN(format, args...)
Log a warning.
Definition: qoflog.h:250
error in response from server
Definition: qofbackend.h:71
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 create_index(const std::string &, const std::string &, const EntryVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
bool begin_transaction() noexcept override
Returns TRUE if successful, false if error.
data in db is corrupt
Definition: qofbackend.h:70
SessionOpenMode
Mode for opening sessions.
Definition: qofsession.h:120
bool add_columns_to_table(const std::string &, const ColVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
Encapsulate the connection to the database.
An iterable wrapper for dbi_result; allows using C++11 range for.
Pure virtual class to iterate over a query result set.
bool create_table(const std::string &, const ColVec &) const noexcept override
Returns TRUE if successful, FALSE if error.
bool rollback_transaction() noexcept override
Returns TRUE if successful, FALSE if error.
int execute_nonselect_statement(const GncSqlStatementPtr &) noexcept override
Returns false if error.
void set_error(QofBackendError err)
Set the error value only if there isn&#39;t already an error already.
Definition: qof-backend.cpp:56