Line data Source code
1 : /********************************************************************
2 : * gnc-backend-dbi.c: load and save data to SQL via libdbi *
3 : * *
4 : * This program is free software; you can redistribute it and/or *
5 : * modify it under the terms of the GNU General Public License as *
6 : * published by the Free Software Foundation; either version 2 of *
7 : * the License, or (at your option) any later version. *
8 : * *
9 : * This program is distributed in the hope that it will be useful, *
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 : * GNU General Public License for more details. *
13 : * *
14 : * You should have received a copy of the GNU General Public License*
15 : * along with this program; if not, contact: *
16 : * *
17 : * Free Software Foundation Voice: +1-617-542-5942 *
18 : * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19 : * Boston, MA 02110-1301, USA gnu@gnu.org *
20 : \********************************************************************/
21 : /** @file gnc-backend-dbi.c
22 : * @brief load and save data to SQL
23 : * @author Copyright (c) 2006-2008 Phil Longstaff <plongstaff@rogers.com>
24 : *
25 : * This file implements the top-level QofBackend API for saving/
26 : * restoring data to/from an SQL db using libdbi
27 : */
28 : #include <glib.h>
29 : #include <glib/gstdio.h>
30 :
31 : #include "config.h"
32 :
33 : #include <platform.h>
34 : #if PLATFORM(WINDOWS)
35 : #include <winsock2.h>
36 : #include <windows.h>
37 : #endif
38 :
39 : #include <inttypes.h>
40 : #include <errno.h>
41 : #include "qof.h"
42 : #include "qofquery-p.h"
43 : #include "qofquerycore-p.h"
44 : #include "Account.h"
45 : #include "TransLog.h"
46 : #include "gnc-engine.h"
47 : #include "SX-book.h"
48 : #include "Recurrence.h"
49 : #include <gnc-features.h>
50 : #include "gnc-uri-utils.h"
51 : #include "gnc-filepath-utils.h"
52 : #include <gnc-path.h>
53 : #include "gnc-locale-utils.h"
54 :
55 : #include "gnc-prefs.h"
56 :
57 : #ifdef S_SPLINT_S
58 : #include "splint-defs.h"
59 : #endif
60 :
61 : #include <boost/regex.hpp>
62 : #include <string>
63 : #include <iomanip>
64 :
65 : #include <qofsession.hpp>
66 : #include <gnc-backend-prov.hpp>
67 : #include "gnc-backend-dbi.h"
68 : #include "gnc-backend-dbi.hpp"
69 :
70 : #include <gnc-sql-object-backend.hpp>
71 : #include "gnc-dbisqlresult.hpp"
72 : #include "gnc-dbisqlconnection.hpp"
73 :
74 : #if LIBDBI_VERSION >= 900
75 : #define HAVE_LIBDBI_R 1
76 : static dbi_inst dbi_instance = nullptr;
77 : #else
78 : #define HAVE_LIBDBI_R 0
79 : #define HAVE_LIBDBI_TO_LONGLONG 0
80 : #endif
81 :
82 : #define TRANSACTION_NAME "trans"
83 :
84 : static QofLogModule log_module = G_LOG_DOMAIN;
85 :
86 : #define FILE_URI_TYPE "file"
87 : #define FILE_URI_PREFIX (FILE_URI_TYPE "://")
88 : #define SQLITE3_URI_TYPE "sqlite3"
89 : #define SQLITE3_URI_PREFIX (SQLITE3_URI_TYPE "://")
90 : #define PGSQL_DEFAULT_PORT 5432
91 :
92 : static void adjust_sql_options (dbi_conn connection);
93 : template<DbType Type> bool save_may_clobber_data (dbi_conn conn,
94 : const std::string& dbname);
95 :
96 : template <DbType Type>
97 : class QofDbiBackendProvider : public QofBackendProvider
98 : {
99 : public:
100 20 : QofDbiBackendProvider (const char* name, const char* type) :
101 20 : QofBackendProvider {name, type} {}
102 : QofDbiBackendProvider(QofDbiBackendProvider&) = delete;
103 : QofDbiBackendProvider operator=(QofDbiBackendProvider&) = delete;
104 : QofDbiBackendProvider(QofDbiBackendProvider&&) = delete;
105 : QofDbiBackendProvider operator=(QofDbiBackendProvider&&) = delete;
106 4 : ~QofDbiBackendProvider () = default;
107 0 : QofBackend* create_backend(void)
108 : {
109 0 : return new GncDbiBackend<Type>(nullptr, nullptr);
110 : }
111 0 : bool type_check(const char* type) { return true; }
112 : };
113 :
114 : /* ================================================================= */
115 : /* ================================================================= */
116 : struct UriStrings
117 : {
118 0 : UriStrings() = default;
119 : UriStrings(const std::string& uri);
120 0 : ~UriStrings() = default;
121 : std::string basename() const noexcept;
122 : const char* dbname() const noexcept;
123 : std::string quote_dbname(DbType t) const noexcept;
124 : std::string m_protocol;
125 : std::string m_host;
126 : std::string m_dbname;
127 : std::string m_username;
128 : std::string m_password;
129 : std::string m_basename;
130 : int m_portnum;
131 : };
132 :
133 0 : UriStrings::UriStrings(const std::string& uri)
134 : {
135 : gchar *scheme, *host, *username, *password, *dbname;
136 : int portnum;
137 0 : gnc_uri_get_components(uri.c_str(), &scheme, &host, &portnum, &username,
138 : &password, &dbname);
139 0 : m_protocol = std::string{scheme};
140 0 : m_host = std::string{host};
141 0 : if (dbname)
142 0 : m_dbname = std::string{dbname};
143 0 : if (username)
144 0 : m_username = std::string{username};
145 0 : if (password)
146 0 : m_password = std::string{password};
147 0 : m_portnum = portnum;
148 0 : g_free(scheme);
149 0 : g_free(host);
150 0 : g_free(username);
151 0 : g_free(password);
152 0 : g_free(dbname);
153 0 : }
154 :
155 : std::string
156 0 : UriStrings::basename() const noexcept
157 : {
158 0 : return m_protocol + "_" + m_host + "_" + m_username + "_" + m_dbname;
159 : }
160 :
161 : const char*
162 0 : UriStrings::dbname() const noexcept
163 : {
164 0 : return m_dbname.c_str();
165 : }
166 :
167 : std::string
168 0 : UriStrings::quote_dbname(DbType t) const noexcept
169 : {
170 0 : if (m_dbname.empty())
171 0 : return "";
172 0 : const char quote = (t == DbType::DBI_MYSQL ? '`' : '"');
173 0 : std::string retval(1, quote);
174 0 : retval += m_dbname + quote;
175 0 : return retval;
176 0 : }
177 :
178 : static void
179 0 : set_options(dbi_conn conn, const PairVec& options)
180 : {
181 0 : for (const auto& option : options)
182 : {
183 0 : auto opt = option.first.c_str();
184 0 : auto val = option.second.c_str();
185 0 : auto result = dbi_conn_set_option(conn, opt, val);
186 0 : if (result < 0)
187 : {
188 0 : const char *msg = nullptr;
189 0 : dbi_conn_error(conn, &msg);
190 0 : PERR("Error setting %s option to %s: %s", opt, val, msg);
191 0 : throw std::runtime_error(msg);
192 : }
193 : }
194 0 : }
195 :
196 : /**
197 : * Sets standard db options in a dbi_conn.
198 : *
199 : * @param conn dbi_conn connection
200 : * @param uri UriStrings containing the needed parameters.
201 : * @return TRUE if successful, FALSE if error
202 : */
203 : template <DbType Type> bool
204 0 : GncDbiBackend<Type>::set_standard_connection_options (dbi_conn conn,
205 : const UriStrings& uri)
206 :
207 : {
208 0 : PairVec options;
209 0 : options.push_back(std::make_pair("host", uri.m_host));
210 0 : options.push_back(std::make_pair("dbname", uri.m_dbname));
211 0 : options.push_back(std::make_pair("username", uri.m_username));
212 0 : options.push_back(std::make_pair("password", uri.m_password));
213 0 : options.push_back(std::make_pair("encoding", "UTF-8"));
214 : try
215 : {
216 0 : set_options(conn, options);
217 0 : auto result = dbi_conn_set_option_numeric(conn, "port", uri.m_portnum);
218 0 : if (result < 0)
219 : {
220 0 : const char *msg = nullptr;
221 0 : auto err = dbi_conn_error(conn, &msg);
222 0 : PERR("Error (%d) setting port option to %d: %s", err, uri.m_portnum, msg);
223 0 : throw std::runtime_error(msg);
224 : }
225 : }
226 0 : catch (std::runtime_error& err)
227 : {
228 0 : set_error (ERR_BACKEND_SERVER_ERR);
229 0 : return false;
230 : }
231 :
232 0 : return true;
233 0 : }
234 :
235 : template <DbType Type> void error_handler(dbi_conn conn, void* data);
236 : void error_handler(dbi_conn conn, void* data);
237 :
238 : template <DbType Type> dbi_conn
239 0 : GncDbiBackend<Type>::conn_setup (PairVec& options, UriStrings& uri)
240 : {
241 0 : const char* dbstr = (Type == DbType::DBI_SQLITE ? "sqlite3" :
242 : Type == DbType::DBI_MYSQL ? "mysql" : "pgsql");
243 : #if HAVE_LIBDBI_R
244 0 : dbi_conn conn = nullptr;
245 0 : if (dbi_instance)
246 0 : conn = dbi_conn_new_r (dbstr, dbi_instance);
247 : else
248 0 : PERR ("Attempt to connect with an uninitialized dbi_instance");
249 : #else
250 : auto conn = dbi_conn_new (dbstr);
251 : #endif
252 :
253 0 : if (conn == nullptr)
254 : {
255 0 : PERR ("Unable to create %s dbi connection", dbstr);
256 0 : set_error (ERR_BACKEND_BAD_URL);
257 0 : return nullptr;
258 : }
259 :
260 0 : dbi_conn_error_handler (conn, error_handler<Type>, this);
261 0 : if (!uri.m_dbname.empty() &&
262 0 : !set_standard_connection_options(conn, uri))
263 : {
264 0 : dbi_conn_close(conn);
265 0 : return nullptr;
266 : }
267 0 : if(!options.empty())
268 : {
269 : try {
270 0 : set_options(conn, options);
271 : }
272 0 : catch (std::runtime_error& err)
273 : {
274 0 : dbi_conn_close(conn);
275 0 : set_error (ERR_BACKEND_SERVER_ERR);
276 0 : return nullptr;
277 : }
278 : }
279 :
280 0 : return conn;
281 : }
282 :
283 : template <DbType Type>bool
284 0 : GncDbiBackend<Type>::create_database(dbi_conn conn, const char* db)
285 : {
286 : const char *dbname;
287 : const char *dbcreate;
288 : if (Type == DbType::DBI_MYSQL)
289 : {
290 0 : dbname = "mysql";
291 0 : dbcreate = "CREATE DATABASE %s CHARACTER SET utf8";
292 : }
293 : else
294 : {
295 0 : dbname = "postgres";
296 0 : dbcreate = "CREATE DATABASE %s WITH TEMPLATE template0 ENCODING 'UTF8'";
297 : }
298 0 : PairVec options;
299 0 : options.push_back(std::make_pair("dbname", dbname));
300 : try
301 : {
302 0 : set_options(conn, options);
303 : }
304 0 : catch (std::runtime_error& err)
305 : {
306 0 : set_error (ERR_BACKEND_SERVER_ERR);
307 0 : return false;
308 : }
309 :
310 0 : auto result = dbi_conn_connect (conn);
311 0 : if (result < 0)
312 : {
313 0 : PERR ("Unable to connect to %s database", dbname);
314 0 : set_error(ERR_BACKEND_SERVER_ERR);
315 0 : return false;
316 : }
317 : if (Type == DbType::DBI_MYSQL)
318 0 : adjust_sql_options(conn);
319 0 : auto dresult = dbi_conn_queryf (conn, dbcreate, db);
320 0 : if (dresult == nullptr)
321 : {
322 0 : PERR ("Unable to create database '%s'\n", db);
323 0 : set_error (ERR_BACKEND_SERVER_ERR);
324 0 : return false;
325 : }
326 : if (Type == DbType::DBI_PGSQL)
327 : {
328 0 : const char *alterdb = "ALTER DATABASE %s SET "
329 : "standard_conforming_strings TO on";
330 0 : dbi_conn_queryf (conn, alterdb, db);
331 : }
332 0 : dbi_conn_close(conn);
333 0 : conn = nullptr;
334 0 : return true;
335 0 : }
336 :
337 : template <> void
338 0 : error_handler<DbType::DBI_SQLITE> (dbi_conn conn, void* user_data)
339 : {
340 : const char* msg;
341 0 : GncDbiBackend<DbType::DBI_SQLITE> *dbi_be =
342 : static_cast<decltype(dbi_be)>(user_data);
343 0 : int err_num = dbi_conn_error (conn, &msg);
344 : /* BADIDX is raised if we attempt to seek outside of a result. We
345 : * handle that possibility after checking the return value of the
346 : * seek. Having this raise a critical error breaks looping by
347 : * testing for the return value of the seek.
348 : */
349 0 : if (err_num == DBI_ERROR_BADIDX) return;
350 0 : PERR ("DBI error: %s\n", msg);
351 0 : if (dbi_be->connected())
352 0 : dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, false);
353 : }
354 :
355 : template <> void
356 0 : GncDbiBackend<DbType::DBI_SQLITE>::session_begin(QofSession* session,
357 : const char* new_uri,
358 : SessionOpenMode mode)
359 : {
360 : gboolean file_exists;
361 0 : PairVec options;
362 :
363 0 : g_return_if_fail (session != nullptr);
364 0 : g_return_if_fail (new_uri != nullptr);
365 :
366 0 : ENTER (" ");
367 :
368 : /* Remove uri type if present */
369 0 : auto path = gnc_uri_get_path (new_uri);
370 0 : std::string filepath{path};
371 0 : g_free(path);
372 0 : GFileTest ftest = static_cast<decltype (ftest)> (
373 : G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS) ;
374 0 : file_exists = g_file_test (filepath.c_str(), ftest);
375 0 : bool create{mode == SESSION_NEW_STORE || mode == SESSION_NEW_OVERWRITE};
376 0 : if (!create && !file_exists)
377 : {
378 0 : set_error (ERR_FILEIO_FILE_NOT_FOUND);
379 0 : std::string msg{"Sqlite3 file "};
380 0 : set_message (msg + filepath + " not found");
381 0 : PWARN ("Sqlite3 file %s not found", filepath.c_str());
382 0 : LEAVE("Error");
383 0 : return;
384 0 : }
385 :
386 0 : if (create && file_exists)
387 : {
388 0 : if (mode == SESSION_NEW_OVERWRITE)
389 0 : g_unlink (filepath.c_str());
390 : else
391 : {
392 0 : set_error (ERR_BACKEND_STORE_EXISTS);
393 0 : auto msg = "Might clobber, mode not SESSION_NEW_OVERWRITE";
394 0 : PWARN ("%s", msg);
395 0 : LEAVE("Error");
396 0 : return;
397 : }
398 : }
399 :
400 0 : connect(nullptr);
401 : /* dbi-sqlite3 documentation says that sqlite3 doesn't take a "host" option */
402 0 : options.push_back(std::make_pair("host", "localhost"));
403 0 : auto dirname = g_path_get_dirname (filepath.c_str());
404 0 : auto basename = g_path_get_basename (filepath.c_str());
405 0 : options.push_back(std::make_pair("dbname", basename));
406 0 : options.push_back(std::make_pair("sqlite3_dbdir", dirname));
407 0 : if (basename != nullptr) g_free (basename);
408 0 : if (dirname != nullptr) g_free (dirname);
409 0 : UriStrings uri;
410 0 : auto conn = conn_setup(options, uri);
411 0 : if (conn == nullptr)
412 : {
413 0 : LEAVE("Error");
414 0 : return;
415 : }
416 :
417 0 : auto result = dbi_conn_connect (conn);
418 :
419 0 : if (result < 0)
420 : {
421 0 : dbi_conn_close(conn);
422 0 : PERR ("Unable to connect to %s: %d\n", new_uri, result);
423 0 : set_error (ERR_BACKEND_BAD_URL);
424 0 : LEAVE("Error");
425 0 : return;
426 : }
427 :
428 0 : if (mode != SESSION_READ_ONLY && !conn_test_dbi_library(conn))
429 : {
430 0 : if (create && !file_exists)
431 : {
432 : /* File didn't exist before, but it does now, and we don't want to
433 : * leave it lying around.
434 : */
435 0 : dbi_conn_close (conn);
436 0 : conn = nullptr;
437 0 : g_unlink (filepath.c_str());
438 : }
439 0 : dbi_conn_close(conn);
440 0 : LEAVE("Bad DBI Library");
441 0 : return;
442 : }
443 :
444 : try
445 : {
446 0 : connect(new GncDbiSqlConnection(DbType::DBI_SQLITE,
447 0 : this, conn, mode));
448 : }
449 0 : catch (std::runtime_error& err)
450 : {
451 0 : return;
452 0 : }
453 :
454 : /* We should now have a proper session set up.
455 : * Let's start logging */
456 0 : xaccLogSetBaseName (filepath.c_str());
457 0 : PINFO ("logpath=%s", filepath.c_str() ? filepath.c_str() : "(null)");
458 0 : LEAVE ("");
459 0 : }
460 :
461 :
462 : template <> void
463 0 : error_handler<DbType::DBI_MYSQL> (dbi_conn conn, void* user_data)
464 : {
465 0 : GncDbiBackend<DbType::DBI_MYSQL>* dbi_be =
466 : static_cast<decltype(dbi_be)>(user_data);
467 : const char* msg;
468 :
469 0 : auto err_num = dbi_conn_error (conn, &msg);
470 : /* BADIDX is raised if we attempt to seek outside of a result. We
471 : * handle that possibility after checking the return value of the
472 : * seek. Having this raise a critical error breaks looping by
473 : * testing for the return value of the seek.
474 : */
475 0 : if (err_num == DBI_ERROR_BADIDX) return;
476 :
477 : /* Note: the sql connection may not have been initialized yet
478 : * so let's be careful with using it
479 : */
480 :
481 : /* Database doesn't exist. When this error is triggered the
482 : * GncDbiSqlConnection may not exist yet either, so don't use it here
483 : */
484 0 : if (err_num == 1049) // Database doesn't exist
485 : {
486 0 : PINFO ("DBI error: %s\n", msg);
487 0 : dbi_be->set_exists(false);
488 0 : return;
489 : }
490 :
491 : /* All the other error handling code assumes the GncDbiSqlConnection
492 : * has been initialized. So let's assert it exits here, otherwise
493 : * simply return.
494 : */
495 0 : if (!dbi_be->connected())
496 : {
497 0 : PINFO ("DBI error: %s\n", msg);
498 0 : PINFO ("Note: GncDbiSqlConnection not yet initialized. Skipping further error processing.");
499 0 : return;
500 : }
501 :
502 : /* Test for other errors */
503 0 : if (err_num == 2006) // Server has gone away
504 : {
505 0 : PINFO ("DBI error: %s - Reconnecting...\n", msg);
506 0 : dbi_be->set_dbi_error (ERR_BACKEND_CONN_LOST, 1, true);
507 0 : dbi_be->retry_connection(msg);
508 : }
509 0 : else if (err_num == 2003) // Unable to connect
510 : {
511 0 : dbi_be->set_dbi_error (ERR_BACKEND_CANT_CONNECT, 1, true);
512 0 : dbi_be->retry_connection (msg);
513 : }
514 0 : else if (err_num == 1007) //Database exists
515 : {
516 0 : dbi_be->set_exists(true);
517 0 : return;
518 : }
519 :
520 : else // Any other error
521 : {
522 0 : PERR ("DBI error: %s\n", msg);
523 0 : dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, FALSE);
524 : }
525 : }
526 :
527 : #define SQL_OPTION_TO_REMOVE "NO_ZERO_DATE"
528 :
529 : /* Given an sql_options string returns a copy of the string adjusted as
530 : * necessary. In particular if string the contains SQL_OPTION_TO_REMOVE it is
531 : * removed along with comma separator.
532 : */
533 : std::string
534 0 : adjust_sql_options_string(const std::string& str)
535 : {
536 : /* Regex that finds the SQL_OPTION_TO_REMOVE as the first, last, or middle of a
537 : * comma-delimited list.
538 : */
539 : static const boost::regex reg{"(?:," SQL_OPTION_TO_REMOVE "$|\\b"
540 0 : SQL_OPTION_TO_REMOVE "\\b,?)"};
541 0 : return regex_replace(str, reg, std::string{""});
542 : }
543 :
544 : /* checks mysql sql_options and adjusts if necessary */
545 : static void
546 0 : adjust_sql_options (dbi_conn connection)
547 : {
548 0 : dbi_result result = dbi_conn_query( connection, "SELECT @@sql_mode");
549 0 : if (result == nullptr)
550 : {
551 : const char* errmsg;
552 0 : int err = dbi_conn_error(connection, &errmsg);
553 0 : PERR("Unable to read sql_mode %d : %s", err, errmsg);
554 0 : return;
555 : }
556 0 : dbi_result_first_row(result);
557 0 : std::string str{dbi_result_get_string_idx(result, 1)};
558 0 : dbi_result_free(result);
559 0 : if (str.empty())
560 : {
561 : const char* errmsg;
562 0 : int err = dbi_conn_error(connection, &errmsg);
563 0 : if (err)
564 0 : PERR("Unable to get sql_mode %d : %s", err, errmsg);
565 : else
566 0 : PINFO("Sql_mode isn't set.");
567 0 : return;
568 : }
569 0 : PINFO("Initial sql_mode: %s", str.c_str());
570 0 : if(str.find(SQL_OPTION_TO_REMOVE) != std::string::npos)
571 0 : str = adjust_sql_options_string(str);
572 :
573 : //https://bugs.gnucash.org/show_bug.cgi?id=798112
574 0 : const char* backslash_option{"NO_BACKSLASH_ESCAPES"};
575 :
576 0 : if (str.find(backslash_option) == std::string::npos)
577 : {
578 0 : if (!str.empty())
579 0 : str.append(",");
580 0 : str.append(backslash_option);
581 : }
582 :
583 0 : PINFO("Setting sql_mode to %s", str.c_str());
584 0 : std::string set_str{"SET sql_mode='" + std::move(str) + "'"};
585 0 : dbi_result set_result = dbi_conn_query(connection,
586 : set_str.c_str());
587 0 : if (set_result)
588 : {
589 0 : dbi_result_free(set_result);
590 : }
591 : else
592 : {
593 : const char* errmsg;
594 0 : int err = dbi_conn_error(connection, &errmsg);
595 0 : PERR("Unable to set sql_mode %d : %s", err, errmsg);
596 : }
597 0 : }
598 :
599 : template <DbType Type> bool
600 0 : drop_database(dbi_conn conn, const UriStrings& uri)
601 : {
602 : const char *root_db;
603 : if (Type == DbType::DBI_PGSQL)
604 : {
605 0 : root_db = "template1";
606 : }
607 : else if (Type == DbType::DBI_MYSQL)
608 : {
609 0 : root_db = "mysql";
610 : }
611 : else
612 : {
613 : PERR ("Unknown database type, can't proceed.");
614 : LEAVE("Error");
615 : return false;
616 : }
617 0 : if (dbi_conn_select_db (conn, root_db) == -1)
618 : {
619 0 : PERR ("Failed to switch out of %s, drop will fail.",
620 : uri.quote_dbname(Type).c_str());
621 0 : LEAVE ("Error");
622 0 : return false;
623 : }
624 0 : if (!dbi_conn_queryf (conn, "DROP DATABASE %s",
625 0 : uri.quote_dbname(Type).c_str()))
626 : {
627 0 : PERR ("Failed to drop database %s prior to recreating it."
628 : "Proceeding would combine old and new data.",
629 : uri.quote_dbname(Type).c_str());
630 0 : LEAVE ("Error");
631 0 : return false;
632 : }
633 0 : return true;
634 : }
635 :
636 : template <DbType Type> void
637 0 : GncDbiBackend<Type>::session_begin (QofSession* session, const char* new_uri,
638 : SessionOpenMode mode)
639 : {
640 0 : PairVec options;
641 :
642 0 : g_return_if_fail (session != nullptr);
643 0 : g_return_if_fail (new_uri != nullptr);
644 :
645 0 : ENTER (" ");
646 :
647 : /* Split the book-id
648 : * Format is protocol://username:password@hostname:port/dbname
649 : where username, password and port are optional) */
650 0 : UriStrings uri(new_uri);
651 :
652 : if (Type == DbType::DBI_PGSQL)
653 : {
654 0 : if (uri.m_portnum == 0)
655 0 : uri.m_portnum = PGSQL_DEFAULT_PORT;
656 : /* Postgres's SQL interface coerces identifiers to lower case, but the
657 : * C interface is case-sensitive. This results in a mixed-case dbname
658 : * being created (with a lower case name) but then dbi can't connect to
659 : * it. To work around this, coerce the name to lowercase first. */
660 0 : auto lcname = g_utf8_strdown (uri.dbname(), -1);
661 0 : uri.m_dbname = std::string{lcname};
662 0 : g_free(lcname);
663 : }
664 0 : connect(nullptr);
665 :
666 0 : bool create{mode == SESSION_NEW_STORE || mode == SESSION_NEW_OVERWRITE};
667 0 : auto conn = conn_setup(options, uri);
668 0 : if (conn == nullptr)
669 : {
670 0 : LEAVE("Error");
671 0 : return;
672 : }
673 :
674 0 : m_exists = true; //May be unset in the error handler.
675 0 : auto result = dbi_conn_connect (conn);
676 0 : if (result == 0)
677 : {
678 : if (Type == DbType::DBI_MYSQL)
679 0 : adjust_sql_options (conn);
680 0 : if (mode != SESSION_READ_ONLY && !conn_test_dbi_library(conn))
681 : {
682 0 : dbi_conn_close(conn);
683 0 : LEAVE("Error");
684 0 : return;
685 : }
686 0 : bool create = (mode == SESSION_NEW_STORE ||
687 : mode == SESSION_NEW_OVERWRITE);
688 0 : if (create && save_may_clobber_data<Type>(conn, uri.quote_dbname(Type)))
689 : {
690 0 : if (mode == SESSION_NEW_OVERWRITE)
691 : {
692 0 : if (!drop_database<Type>(conn, uri))
693 0 : return;
694 : }
695 : else
696 : {
697 0 : set_error (ERR_BACKEND_STORE_EXISTS);
698 0 : PWARN ("Database already exists, Might clobber it.");
699 0 : dbi_conn_close(conn);
700 0 : LEAVE("Error");
701 0 : return;
702 : }
703 : /* Drop successful. */
704 0 : m_exists = false;
705 : }
706 :
707 : }
708 0 : else if (m_exists)
709 : {
710 0 : PERR ("Unable to connect to database '%s'\n", uri.dbname());
711 0 : set_error (ERR_BACKEND_CANT_CONNECT);
712 0 : dbi_conn_close(conn);
713 0 : LEAVE("Error");
714 0 : return;
715 : }
716 0 : else if (!create)
717 : {
718 0 : PERR ("Database '%s' does not exist\n", uri.dbname());
719 0 : set_error(ERR_BACKEND_NO_SUCH_DB);
720 0 : std::string msg{"Database "};
721 0 : set_message(msg + uri.dbname() + " not found");
722 0 : LEAVE("Error");
723 0 : return;
724 0 : }
725 :
726 0 : if (create)
727 : {
728 0 : if (!m_exists &&
729 0 : !create_database(conn, uri.quote_dbname(Type).c_str()))
730 : {
731 0 : dbi_conn_close(conn);
732 0 : LEAVE("Error");
733 0 : return;
734 : }
735 0 : conn = conn_setup(options, uri);
736 0 : result = dbi_conn_connect (conn);
737 0 : if (result < 0)
738 : {
739 0 : PERR ("Unable to create database '%s'\n", uri.dbname());
740 0 : set_error (ERR_BACKEND_SERVER_ERR);
741 0 : dbi_conn_close(conn);
742 0 : LEAVE("Error");
743 0 : return;
744 : }
745 : if (Type == DbType::DBI_MYSQL)
746 0 : adjust_sql_options (conn);
747 0 : if (!conn_test_dbi_library(conn))
748 : {
749 : if (Type == DbType::DBI_PGSQL)
750 0 : dbi_conn_select_db (conn, "template1");
751 0 : dbi_conn_queryf (conn, "DROP DATABASE %s",
752 0 : uri.quote_dbname(Type).c_str());
753 0 : dbi_conn_close(conn);
754 0 : return;
755 : }
756 : }
757 :
758 0 : connect(nullptr);
759 : try
760 : {
761 0 : connect(new GncDbiSqlConnection(Type, this, conn, mode));
762 : }
763 0 : catch (std::runtime_error& err)
764 : {
765 0 : return;
766 : }
767 : /* We should now have a proper session set up.
768 : * Let's start logging */
769 0 : auto translog_path = gnc_build_translog_path (uri.basename().c_str());
770 0 : xaccLogSetBaseName (translog_path);
771 0 : PINFO ("logpath=%s", translog_path ? translog_path : "(null)");
772 0 : g_free (translog_path);
773 :
774 0 : LEAVE (" ");
775 0 : }
776 :
777 : template<> void
778 0 : error_handler<DbType::DBI_PGSQL> (dbi_conn conn, void* user_data)
779 : {
780 0 : GncDbiBackend<DbType::DBI_PGSQL>* dbi_be =
781 : static_cast<decltype(dbi_be)>(user_data);
782 : const char* msg;
783 :
784 0 : auto err_num = dbi_conn_error (conn, &msg);
785 : /* BADIDX is raised if we attempt to seek outside of a result. We
786 : * handle that possibility after checking the return value of the
787 : * seek. Having this raise a critical error breaks looping by
788 : * testing for the return value of the seek.
789 : */
790 0 : if (err_num == DBI_ERROR_BADIDX) return;
791 0 : if (g_str_has_prefix (msg, "FATAL: database") &&
792 0 : g_str_has_suffix (msg, "does not exist\n"))
793 : {
794 0 : PINFO ("DBI error: %s\n", msg);
795 0 : dbi_be->set_exists(false);
796 : }
797 0 : else if (g_strrstr (msg,
798 : "server closed the connection unexpectedly")) // Connection lost
799 : {
800 0 : if (!dbi_be->connected())
801 : {
802 0 : PWARN ("DBI Error: Connection lost, connection pointer invalid");
803 0 : return;
804 : }
805 0 : PINFO ("DBI error: %s - Reconnecting...\n", msg);
806 0 : dbi_be->set_dbi_error (ERR_BACKEND_CONN_LOST, 1, true);
807 0 : dbi_be->retry_connection(msg);
808 : }
809 0 : else if (g_str_has_prefix (msg, "connection pointer is NULL") ||
810 0 : g_str_has_prefix (msg, "could not connect to server")) // No connection
811 : {
812 :
813 0 : if (!dbi_be->connected())
814 0 : qof_backend_set_error(reinterpret_cast<QofBackend*>(dbi_be),
815 : ERR_BACKEND_CANT_CONNECT);
816 : else
817 : {
818 0 : dbi_be->set_dbi_error(ERR_BACKEND_CANT_CONNECT, 1, true);
819 0 : dbi_be->retry_connection (msg);
820 : }
821 : }
822 : else
823 : {
824 0 : PERR ("DBI error: %s\n", msg);
825 0 : if (dbi_be->connected())
826 0 : dbi_be->set_dbi_error (ERR_BACKEND_MISC, 0, false);
827 : }
828 : }
829 :
830 : /* ================================================================= */
831 :
832 : template <DbType Type> void
833 0 : GncDbiBackend<Type>::session_end ()
834 : {
835 0 : ENTER (" ");
836 :
837 0 : finalize_version_info ();
838 0 : connect(nullptr);
839 :
840 0 : LEAVE (" ");
841 0 : }
842 :
843 : template <DbType Type>
844 0 : GncDbiBackend<Type>::~GncDbiBackend()
845 : {
846 : /* Stop transaction logging */
847 0 : xaccLogSetBaseName (nullptr);
848 0 : }
849 :
850 : /* ================================================================= */
851 :
852 : /* GNUCASH_RESAVE_VERSION indicates the earliest database version
853 : * compatible with this version of Gnucash; the stored value is the
854 : * earliest version of Gnucash conpatible with the database. If the
855 : * GNUCASH_RESAVE_VERSION for this Gnucash is newer than the Gnucash
856 : * version which created the database, a resave is offered. If the
857 : * version of this Gnucash is older than the saved resave version,
858 : * then the database will be loaded read-only. A resave will update
859 : * both values to match this version of Gnucash.
860 : */
861 : template <DbType Type> void
862 0 : GncDbiBackend<Type>::load (QofBook* book, QofBackendLoadType loadType)
863 : {
864 0 : g_return_if_fail (book != nullptr);
865 :
866 0 : ENTER ("dbi_be=%p, book=%p", this, book);
867 :
868 0 : if (loadType == LOAD_TYPE_INITIAL_LOAD)
869 : {
870 :
871 : // Set up table version information
872 0 : init_version_info ();
873 0 : assert (m_book == nullptr);
874 0 : create_tables();
875 : }
876 :
877 0 : GncSqlBackend::load(book, loadType);
878 :
879 : if (Type == DbType::DBI_SQLITE)
880 0 : gnc_features_set_used(book, GNC_FEATURE_SQLITE3_ISO_DATES);
881 :
882 0 : if (GNUCASH_RESAVE_VERSION > get_table_version("Gnucash"))
883 : {
884 : /* The database was loaded with an older database schema or
885 : * data semantics. In order to ensure consistency, the whole
886 : * thing needs to be saved anew. */
887 0 : set_error(ERR_SQL_DB_TOO_OLD);
888 : }
889 0 : else if (GNUCASH_RESAVE_VERSION < get_table_version("Gnucash-Resave"))
890 : {
891 : /* Worse, the database was created with a newer version. We
892 : * can't safely write to this database, so the user will have
893 : * to do a "save as" to make one that we can write to.
894 : */
895 0 : set_error(ERR_SQL_DB_TOO_NEW);
896 : }
897 :
898 :
899 0 : LEAVE ("");
900 : }
901 :
902 : /* ================================================================= */
903 : /* This is used too early to call GncDbiProvider::get_table_list(). */
904 : template <DbType T> bool
905 0 : save_may_clobber_data (dbi_conn conn, const std::string& dbname)
906 : {
907 :
908 : /* Data may be clobbered iff the number of tables != 0 */
909 0 : auto result = dbi_conn_get_table_list (conn, dbname.c_str(), nullptr);
910 0 : bool retval = false;
911 0 : if (result)
912 : {
913 0 : retval = dbi_result_get_numrows (result) > 0;
914 0 : dbi_result_free (result);
915 : }
916 0 : return retval;
917 : }
918 :
919 : template <> bool
920 0 : save_may_clobber_data <DbType::DBI_PGSQL>(dbi_conn conn,
921 : const std::string& dbname)
922 : {
923 :
924 : /* Data may be clobbered iff the number of tables != 0 */
925 0 : const char* query = "SELECT relname FROM pg_class WHERE relname !~ '^(pg|sql)_' AND relkind = 'r' ORDER BY relname";
926 0 : auto result = dbi_conn_query (conn, query);
927 0 : bool retval = false;
928 0 : if (result)
929 : {
930 0 : retval = dbi_result_get_numrows (result) > 0;
931 0 : dbi_result_free (result);
932 : }
933 0 : return retval;
934 : }
935 :
936 :
937 : /**
938 : * Safely resave a database by renaming all of its tables, recreating
939 : * everything, and then dropping the backup tables only if there were
940 : * no errors. If there are errors, drop the new tables and restore the
941 : * originals.
942 : *
943 : * @param book: QofBook to be saved in the database.
944 : */
945 : template <DbType Type> void
946 0 : GncDbiBackend<Type>::safe_sync (QofBook* book)
947 : {
948 0 : auto conn = dynamic_cast<GncDbiSqlConnection*>(m_conn);
949 :
950 0 : g_return_if_fail (conn != nullptr);
951 0 : g_return_if_fail (book != nullptr);
952 :
953 0 : ENTER ("book=%p, primary=%p", book, m_book);
954 0 : if (!conn->begin_transaction())
955 : {
956 0 : LEAVE("Failed to obtain a transaction.");
957 0 : return;
958 : }
959 0 : if (!conn->table_operation (TableOpType::backup))
960 : {
961 0 : conn->rollback_transaction();
962 0 : LEAVE ("Failed to rename tables");
963 0 : return;
964 : }
965 0 : if (!conn->drop_indexes())
966 : {
967 0 : conn->rollback_transaction();
968 0 : LEAVE ("Failed to drop indexes");
969 0 : return;
970 : }
971 :
972 0 : sync(m_book);
973 0 : if (check_error())
974 : {
975 0 : conn->rollback_transaction();
976 0 : LEAVE ("Failed to create new database tables");
977 0 : return;
978 : }
979 0 : conn->commit_transaction();
980 0 : conn->table_operation (TableOpType::drop_backup);
981 0 : LEAVE ("book=%p", m_book);
982 : }
983 : /* MySQL commits the transaction and all savepoints after the first CREATE
984 : * TABLE, crashing when we try to RELEASE SAVEPOINT because the savepoint
985 : * doesn't exist after the commit. We must run without a wrapping transaction in
986 : * that case.
987 : */
988 : template <> void
989 0 : GncDbiBackend<DbType::DBI_MYSQL>::safe_sync (QofBook* book)
990 : {
991 0 : auto conn = dynamic_cast<GncDbiSqlConnection*>(m_conn);
992 :
993 0 : g_return_if_fail (conn != nullptr);
994 0 : g_return_if_fail (book != nullptr);
995 :
996 0 : ENTER ("book=%p, primary=%p", book, m_book);
997 0 : if (!conn->table_operation (TableOpType::backup))
998 : {
999 0 : set_error(ERR_BACKEND_SERVER_ERR);
1000 0 : conn->table_operation (TableOpType::rollback);
1001 0 : LEAVE ("Failed to rename tables");
1002 0 : return;
1003 : }
1004 0 : if (!conn->drop_indexes())
1005 : {
1006 0 : conn->table_operation (TableOpType::rollback);
1007 0 : set_error (ERR_BACKEND_SERVER_ERR);
1008 0 : set_message("Failed to drop indexes");
1009 0 : LEAVE ("Failed to drop indexes");
1010 0 : return;
1011 : }
1012 :
1013 0 : sync(m_book);
1014 0 : if (check_error())
1015 : {
1016 0 : conn->table_operation (TableOpType::rollback);
1017 0 : LEAVE ("Failed to create new database tables");
1018 0 : return;
1019 : }
1020 0 : conn->table_operation (TableOpType::drop_backup);
1021 0 : LEAVE ("book=%p", m_book);
1022 : }
1023 : /* ================================================================= */
1024 :
1025 : /*
1026 : * Checks to see whether the file is an sqlite file or not
1027 : *
1028 : */
1029 : template<> bool
1030 2 : QofDbiBackendProvider<DbType::DBI_SQLITE>::type_check(const char *uri)
1031 : {
1032 : FILE* f;
1033 2 : gchar buf[51]{};
1034 : G_GNUC_UNUSED size_t chars_read;
1035 : gint status;
1036 : gchar* filename;
1037 :
1038 : // BAD if the path is null
1039 2 : g_return_val_if_fail (uri != nullptr, FALSE);
1040 :
1041 2 : filename = gnc_uri_get_path (uri);
1042 2 : f = g_fopen (filename, "r");
1043 2 : g_free (filename);
1044 :
1045 : // OK if the file doesn't exist - new file
1046 2 : if (f == nullptr)
1047 : {
1048 0 : PINFO ("doesn't exist (errno=%d) -> DBI", errno);
1049 0 : return TRUE;
1050 : }
1051 :
1052 : // OK if file has the correct header
1053 2 : chars_read = fread (buf, sizeof (buf) - 1, 1, f);
1054 2 : status = fclose (f);
1055 2 : if (status < 0)
1056 : {
1057 0 : PERR ("Error in fclose(): %d\n", errno);
1058 : }
1059 2 : if (g_str_has_prefix (buf, "SQLite format 3"))
1060 : {
1061 0 : PINFO ("has SQLite format string -> DBI");
1062 0 : return TRUE;
1063 : }
1064 2 : PINFO ("exists, does not have SQLite format string -> not DBI");
1065 :
1066 : // Otherwise, BAD
1067 2 : return FALSE;
1068 : }
1069 :
1070 : void
1071 10 : gnc_module_init_backend_dbi (void)
1072 : {
1073 : const char* driver_dir;
1074 : int num_drivers;
1075 10 : gboolean have_sqlite3_driver = FALSE;
1076 10 : gboolean have_mysql_driver = FALSE;
1077 10 : gboolean have_pgsql_driver = FALSE;
1078 :
1079 : /* Initialize libdbi and see which drivers are available. Only register qof backends which
1080 : have drivers available. */
1081 10 : driver_dir = g_getenv ("GNC_DBD_DIR");
1082 10 : if (driver_dir == nullptr)
1083 : {
1084 10 : PINFO ("GNC_DBD_DIR not set: using libdbi built-in default\n");
1085 : }
1086 :
1087 : /* dbi_initialize returns -1 in case of errors */
1088 : #if HAVE_LIBDBI_R
1089 10 : if (dbi_instance)
1090 0 : return;
1091 10 : num_drivers = dbi_initialize_r (driver_dir, &dbi_instance);
1092 : #else
1093 : num_drivers = dbi_initialize (driver_dir);
1094 : #endif
1095 10 : if (num_drivers <= 0)
1096 : {
1097 : #if HAVE_LIBDBI_R
1098 0 : if (dbi_instance)
1099 : {
1100 0 : dbi_shutdown_r (dbi_instance);
1101 0 : dbi_instance = nullptr;
1102 : }
1103 : #endif
1104 0 : gchar *libdir = gnc_path_get_libdir ();
1105 0 : gchar *dir = g_build_filename (libdir, "dbd", nullptr);
1106 0 : g_free (libdir);
1107 : #if HAVE_LIBDBI_R
1108 0 : num_drivers = dbi_initialize_r (dir, &dbi_instance);
1109 : #else
1110 : num_drivers = dbi_initialize (dir);
1111 : #endif
1112 0 : g_free (dir);
1113 : }
1114 10 : if (num_drivers <= 0)
1115 : {
1116 0 : PWARN ("No DBD drivers found\n");
1117 : }
1118 : else
1119 : {
1120 10 : dbi_driver driver = nullptr;
1121 10 : PINFO ("%d DBD drivers found\n", num_drivers);
1122 :
1123 : do
1124 : {
1125 : #if HAVE_LIBDBI_R
1126 20 : driver = dbi_driver_list_r (driver, dbi_instance);
1127 : #else
1128 : driver = dbi_driver_list (driver);
1129 : #endif
1130 :
1131 20 : if (driver != nullptr)
1132 : {
1133 10 : const gchar* name = dbi_driver_get_name (driver);
1134 :
1135 10 : PINFO ("Driver: %s\n", name);
1136 10 : if (strcmp (name, "sqlite3") == 0)
1137 : {
1138 10 : have_sqlite3_driver = TRUE;
1139 : }
1140 0 : else if (strcmp (name, "mysql") == 0)
1141 : {
1142 0 : have_mysql_driver = TRUE;
1143 : }
1144 0 : else if (strcmp (name, "pgsql") == 0)
1145 : {
1146 0 : have_pgsql_driver = TRUE;
1147 : }
1148 : }
1149 : }
1150 20 : while (driver != nullptr);
1151 : }
1152 :
1153 10 : if (have_sqlite3_driver)
1154 : {
1155 10 : const char* name = "GnuCash Libdbi (SQLITE3) Backend";
1156 10 : auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_SQLITE>{name, FILE_URI_TYPE});
1157 10 : qof_backend_register_provider(std::move(prov));
1158 10 : prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_SQLITE>{name, SQLITE3_URI_TYPE});
1159 10 : qof_backend_register_provider(std::move(prov));
1160 10 : }
1161 :
1162 10 : if (have_mysql_driver)
1163 : {
1164 0 : const char *name = "GnuCash Libdbi (MYSQL) Backend";
1165 0 : auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_MYSQL>{name, "mysql"});
1166 0 : qof_backend_register_provider(std::move(prov));
1167 0 : }
1168 :
1169 10 : if (have_pgsql_driver)
1170 : {
1171 0 : const char* name = "GnuCash Libdbi (POSTGRESQL) Backend";
1172 0 : auto prov = QofBackendProvider_ptr(new QofDbiBackendProvider<DbType::DBI_PGSQL>{name, "postgres"});
1173 0 : qof_backend_register_provider(std::move(prov));
1174 0 : }
1175 :
1176 : /* If needed, set log level to DEBUG so that SQl statements will be put into
1177 : the gnucash.trace file. */
1178 : /* qof_log_set_level( log_module, QOF_LOG_DEBUG ); */
1179 : }
1180 :
1181 : #ifndef GNC_NO_LOADABLE_MODULES
1182 : G_MODULE_EXPORT void
1183 11 : qof_backend_module_init (void)
1184 : {
1185 11 : gnc_module_init_backend_dbi ();
1186 11 : }
1187 :
1188 : G_MODULE_EXPORT void
1189 0 : qof_backend_module_finalize (void)
1190 : {
1191 0 : gnc_module_finalize_backend_dbi ();
1192 0 : }
1193 : #endif /* GNC_NO_LOADABLE_MODULES */
1194 :
1195 : void
1196 0 : gnc_module_finalize_backend_dbi (void)
1197 : {
1198 : #if HAVE_LIBDBI_R
1199 0 : if (dbi_instance)
1200 : {
1201 0 : dbi_shutdown_r (dbi_instance);
1202 0 : dbi_instance = nullptr;
1203 : }
1204 : #else
1205 : dbi_shutdown ();
1206 : #endif
1207 0 : }
1208 :
1209 : /* --------------------------------------------------------- */
1210 :
1211 : static void
1212 0 : log_failed_field(dbi_result result, const char* fieldname)
1213 : {
1214 0 : auto idx = dbi_result_get_field_idx(result, fieldname);
1215 0 : if (dbi_result_field_is_null_idx(result, idx))
1216 0 : PERR("Result field %s is NULL", fieldname);
1217 : else
1218 : {
1219 0 : auto type = dbi_result_get_field_type_idx(result, idx);
1220 0 : auto attribs = dbi_result_get_field_attribs_idx(result, idx);
1221 0 : PERR("Result field %s has type %d and attribs %d",
1222 : fieldname, type, attribs);
1223 : }
1224 0 : }
1225 :
1226 : /** Users discovered a bug in some distributions of libdbi, where if
1227 : * it is compiled on certain versions of gcc with the -ffast-math
1228 : * compiler option it fails to correctly handle saving of 64-bit
1229 : * values. This function tests for the problem.
1230 : * @param: conn: The just-opened dbi_conn
1231 : * @returns: GNC_DBI_PASS if the dbi library is safe to use,
1232 : * GNC_DBI_FAIL_SETUP if the test could not be completed, or
1233 : * GNC_DBI_FAIL_TEST if the bug was found.
1234 : */
1235 : static GncDbiTestResult
1236 0 : dbi_library_test (dbi_conn conn)
1237 : {
1238 0 : int64_t testlonglong = -9223372036854775807LL, resultlonglong = 0;
1239 0 : uint64_t testulonglong = 9223372036854775807LLU, resultulonglong = 0;
1240 0 : double testdouble = 1.7976921348623157E+307, resultdouble = 0.0;
1241 : dbi_result result;
1242 0 : GncDbiTestResult retval = GNC_DBI_PASS;
1243 :
1244 0 : result = dbi_conn_query (conn, "CREATE TEMPORARY TABLE numtest "
1245 : "( test_int BIGINT, test_unsigned BIGINT,"
1246 : " test_double FLOAT8 )");
1247 0 : if (result == nullptr)
1248 : {
1249 0 : PWARN ("Test_DBI_Library: Create table failed");
1250 0 : return GNC_DBI_FAIL_SETUP;
1251 : }
1252 0 : dbi_result_free (result);
1253 0 : std::stringstream querystr;
1254 0 : querystr << "INSERT INTO numtest VALUES (" << testlonglong <<
1255 0 : ", " << testulonglong << ", " << std::setprecision(12) <<
1256 0 : testdouble << ")";
1257 0 : auto query = querystr.str();
1258 0 : result = dbi_conn_query (conn, query.c_str());
1259 0 : if (result == nullptr)
1260 : {
1261 0 : PWARN ("Test_DBI_Library: Failed to insert test row into table");
1262 0 : return GNC_DBI_FAIL_SETUP;
1263 : }
1264 0 : dbi_result_free (result);
1265 0 : auto locale = gnc_push_locale (LC_NUMERIC, "C");
1266 0 : result = dbi_conn_query (conn, "SELECT * FROM numtest");
1267 0 : if (result == nullptr || !dbi_result_get_numrows(result))
1268 : {
1269 : const char* errmsg;
1270 0 : dbi_conn_error (conn, &errmsg);
1271 0 : PWARN ("Test_DBI_Library: Failed to retrieve test row into table: %s",
1272 : errmsg);
1273 0 : dbi_conn_query (conn, "DROP TABLE numtest");
1274 0 : gnc_pop_locale (LC_NUMERIC, locale);
1275 0 : return GNC_DBI_FAIL_SETUP;
1276 : }
1277 0 : while (dbi_result_next_row (result))
1278 : {
1279 0 : resultlonglong = dbi_result_get_longlong (result, "test_int");
1280 0 : if (!resultlonglong)
1281 0 : log_failed_field(result, "test_int");
1282 0 : resultulonglong = dbi_result_get_ulonglong (result, "test_unsigned");
1283 0 : if (!resultulonglong)
1284 0 : log_failed_field(result, "test_unsigned");
1285 0 : resultdouble = dbi_result_get_double (result, "test_double");
1286 0 : if (!resultdouble)
1287 0 : log_failed_field(result, "test_double");
1288 : }
1289 0 : dbi_conn_query (conn, "DROP TABLE numtest");
1290 0 : gnc_pop_locale (LC_NUMERIC, locale);
1291 0 : if (testlonglong != resultlonglong)
1292 : {
1293 0 : PWARN ("Test_DBI_Library: LongLong Failed %" PRId64 " != % " PRId64,
1294 : testlonglong, resultlonglong);
1295 0 : retval = GNC_DBI_FAIL_TEST;
1296 : }
1297 0 : if (testulonglong != resultulonglong)
1298 : {
1299 0 : PWARN ("Test_DBI_Library: Unsigned longlong Failed %" PRIu64 " != %"
1300 : PRIu64, testulonglong, resultulonglong);
1301 0 : retval = GNC_DBI_FAIL_TEST;
1302 : }
1303 : /* A bug in libdbi stores only 7 digits of precision */
1304 0 : if (testdouble >= resultdouble + 0.000001e307 ||
1305 0 : testdouble <= resultdouble - 0.000001e307)
1306 : {
1307 0 : PWARN ("Test_DBI_Library: Double Failed %17e != %17e",
1308 : testdouble, resultdouble);
1309 0 : retval = GNC_DBI_FAIL_TEST;
1310 : }
1311 0 : return retval;
1312 0 : }
1313 :
1314 : template <DbType Type> bool
1315 0 : GncDbiBackend<Type>::conn_test_dbi_library(dbi_conn conn)
1316 : {
1317 0 : auto result = dbi_library_test (conn);
1318 0 : switch (result)
1319 : {
1320 0 : case GNC_DBI_PASS:
1321 0 : break;
1322 :
1323 0 : case GNC_DBI_FAIL_SETUP:
1324 0 : set_error(ERR_SQL_DBI_UNTESTABLE);
1325 0 : set_message ("DBI library large number test incomplete");
1326 0 : break;
1327 :
1328 0 : case GNC_DBI_FAIL_TEST:
1329 0 : set_error (ERR_SQL_BAD_DBI);
1330 0 : set_message ("DBI library fails large number test");
1331 0 : break;
1332 : }
1333 0 : return result == GNC_DBI_PASS;
1334 : }
1335 :
1336 : /* ========================== END OF FILE ===================== */
|