1 /***************************************************************************
2                           mymoneystoragesql.cpp
3                           ---------------------
4     begin                : 11 November 2005
5     copyright            : (C) 2005 by Tony Bloomfield
6     email                : tonybloom@users.sourceforge.net
7                          : Fernando Vilas <fvilas@iname.com>
8                          : Christian Dávid <christian-david@web.de>
9                          (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
10  ***************************************************************************/
11 
12 /***************************************************************************
13  *                                                                         *
14  *   This program is free software; you can redistribute it and/or modify  *
15  *   it under the terms of the GNU General Public License as published by  *
16  *   the Free Software Foundation; either version 2 of the License, or     *
17  *   (at your option) any later version.                                   *
18  *                                                                         *
19  ***************************************************************************/
20 
21 #ifndef MYMONEYSTORAGESQL_P_H
22 #define MYMONEYSTORAGESQL_P_H
23 
24 #include "mymoneystoragesql.h"
25 
26 // ----------------------------------------------------------------------------
27 // System Includes
28 #include <algorithm>
29 
30 // ----------------------------------------------------------------------------
31 // QT Includes
32 
33 #include <QString>
34 #include <QDateTime>
35 #include <QStringList>
36 #include <QIODevice>
37 #include <QUrlQuery>
38 #include <QSqlQuery>
39 #include <QSqlError>
40 #include <QList>
41 #include <QSqlRecord>
42 #include <QMap>
43 #include <QFile>
44 #include <QVariant>
45 #include <QColor>
46 #include <QDebug>
47 #include <QStack>
48 
49 // ----------------------------------------------------------------------------
50 // KDE Includes
51 
52 #include <KLocalizedString>
53 
54 // ----------------------------------------------------------------------------
55 // Project Includes
56 
57 #include "mymoneystoragemgr.h"
58 #include "onlinejobadministration.h"
59 #include "onlinetasks/interfaces/tasks/onlinetask.h"
60 #include "mymoneycostcenter.h"
61 #include "mymoneyexception.h"
62 #include "mymoneyinstitution.h"
63 #include "mymoneyaccount.h"
64 #include "mymoneysecurity.h"
65 #include "mymoneymoney.h"
66 #include "mymoneyschedule.h"
67 #include "mymoneypayee.h"
68 #include "mymoneytag.h"
69 #include "mymoneysplit.h"
70 #include "mymoneytransaction.h"
71 #include "mymoneytransactionfilter.h"
72 #include "mymoneybudget.h"
73 #include "mymoneyreport.h"
74 #include "mymoneyprice.h"
75 #include "mymoneyutils.h"
76 #include "mymoneydbdef.h"
77 #include "mymoneydbdriver.h"
78 #include "payeeidentifierdata.h"
79 #include "payeeidentifier.h"
80 #include "payeeidentifiertyped.h"
81 #include "payeeidentifier/ibanbic/ibanbic.h"
82 #include "payeeidentifier/nationalaccount/nationalaccount.h"
83 #include "onlinetasks/sepa/sepaonlinetransferimpl.h"
84 #include "xmlstoragehelper.h"
85 #include "mymoneyenums.h"
86 
87 using namespace eMyMoney;
88 
89 class FilterFail
90 {
91 public:
FilterFail(const MyMoneyTransactionFilter & filter)92   explicit FilterFail(const MyMoneyTransactionFilter& filter) : m_filter(filter) {}
93 
operator()94   inline bool operator()(const QPair<QString, MyMoneyTransaction>& transactionPair) {
95     return (*this)(transactionPair.second);
96   }
97 
operator()98   inline bool operator()(const MyMoneyTransaction& transaction) {
99     return !m_filter.match(transaction);
100   }
101 
102 private:
103   MyMoneyTransactionFilter m_filter;
104 };
105 
106 //*****************************************************************************
107 // Create a class to handle db transactions using scope
108 //
109 // Don't let the database object get destroyed while this object exists,
110 // that would result in undefined behavior.
111 class MyMoneyDbTransaction
112 {
113 public:
MyMoneyDbTransaction(MyMoneyStorageSql & db,const QString & name)114   explicit MyMoneyDbTransaction(MyMoneyStorageSql& db, const QString& name) :
115     m_db(db), m_name(name)
116   {
117     db.startCommitUnit(name);
118   }
119 
~MyMoneyDbTransaction()120   ~MyMoneyDbTransaction()
121   {
122     if (std::uncaught_exception()) {
123       m_db.cancelCommitUnit(m_name);
124     } else {
125       try{
126         m_db.endCommitUnit(m_name);
127       } catch (const MyMoneyException &) {
128         try {
129           m_db.cancelCommitUnit(m_name);
130         } catch (const MyMoneyException &e) {
131           qDebug() << e.what();
132         }
133       }
134     }
135   }
136 private:
137   MyMoneyStorageSql& m_db;
138   QString m_name;
139 };
140 
141 /**
142   * The MyMoneySqlQuery class is derived from QSqlQuery to provide
143   * a way to adjust some queries based on database type and make
144   * debugging easier by providing a place to put debug statements.
145   */
146 class MyMoneySqlQuery : public QSqlQuery
147 {
148 public:
149   explicit MyMoneySqlQuery(MyMoneyStorageSql* db = 0) :
150     QSqlQuery(*db)
151   {
152   }
153 
~MyMoneySqlQuery()154   virtual ~MyMoneySqlQuery()
155   {
156   }
157 
exec()158   bool exec()
159   {
160     qDebug() << "start sql:" << lastQuery();
161     bool rc = QSqlQuery::exec();
162     qDebug() << "end sql:" << QSqlQuery::executedQuery();
163     qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected();
164     return (rc);
165   }
166 
exec(const QString & query)167   bool exec(const QString & query)
168   {
169     qDebug() << "start sql:" << query;
170     bool rc = QSqlQuery::exec(query);
171     qDebug() << "end sql:" << QSqlQuery::executedQuery();
172     qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected();
173     return rc;
174   }
175 
prepare(const QString & query)176   bool prepare(const QString & query)
177   {
178     return (QSqlQuery::prepare(query));
179   }
180 };
181 
182 #define GETSTRING(a) query.value(a).toString()
183 #define GETDATE(a) getDate(GETSTRING(a))
184 #define GETDATE_D(a) d->getDate(GETSTRING(a))
185 #define GETDATETIME(a) getDateTime(GETSTRING(a))
186 #define GETINT(a) query.value(a).toInt()
187 #define GETULL(a) query.value(a).toULongLong()
188 #define MYMONEYEXCEPTIONSQL(exceptionMessage) MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, exceptionMessage))
189 #define MYMONEYEXCEPTIONSQL_D(exceptionMessage) MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, exceptionMessage))
190 
191 class MyMoneyStorageSqlPrivate
192 {
193   Q_DISABLE_COPY(MyMoneyStorageSqlPrivate)
Q_DECLARE_PUBLIC(MyMoneyStorageSql)194   Q_DECLARE_PUBLIC(MyMoneyStorageSql)
195 
196 public:
197   explicit MyMoneyStorageSqlPrivate(MyMoneyStorageSql* qq) :
198     q_ptr(qq),
199     m_dbVersion(0),
200     m_storage(nullptr),
201     m_loadAll(false),
202     m_override(false),
203     m_institutions(0),
204     m_accounts(0),
205     m_payees(0),
206     m_tags(0),
207     m_transactions(0),
208     m_splits(0),
209     m_securities(0),
210     m_prices(0),
211     m_currencies(0),
212     m_schedules(0),
213     m_reports(0),
214     m_kvps(0),
215     m_budgets(0),
216     m_onlineJobs(0),
217     m_payeeIdentifier(0),
218     m_hiIdInstitutions(0),
219     m_hiIdPayees(0),
220     m_hiIdTags(0),
221     m_hiIdAccounts(0),
222     m_hiIdTransactions(0),
223     m_hiIdSchedules(0),
224     m_hiIdSecurities(0),
225     m_hiIdReports(0),
226     m_hiIdBudgets(0),
227     m_hiIdOnlineJobs(0),
228     m_hiIdPayeeIdentifier(0),
229     m_hiIdCostCenter(0),
230     m_displayStatus(false),
231     m_readingPrices(false),
232     m_newDatabase(false),
233     m_progressCallback(nullptr)
234   {
235     m_preferred.setReportAllSplits(false);
236   }
237 
~MyMoneyStorageSqlPrivate()238   ~MyMoneyStorageSqlPrivate()
239   {
240   }
241 
242   enum class SQLAction {
243     Save,
244     Modify,
245     Remove
246   };
247 
248   /**
249    * MyMoneyStorageSql get highest ID number from the database
250    *
251    * @return : highest ID number
252    */
highestNumberFromIdString(QString tableName,QString tableField,int prefixLength)253   ulong highestNumberFromIdString(QString tableName, QString tableField, int prefixLength)
254   {
255     Q_Q(MyMoneyStorageSql);
256     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
257     QSqlQuery query(*q);
258 
259     if (!query.exec(m_driver->highestNumberFromIdString(tableName, tableField, prefixLength)) || !query.next())
260       throw MYMONEYEXCEPTIONSQL("retrieving highest ID number");
261 
262     return query.value(0).toULongLong();
263   }
264 
265   /**
266    * @name writeFromStorageMethods
267    * @{
268    * These method write all data from m_storage to the database. Data which is
269    * stored in the database is deleted.
270    */
271   void writeUserInformation();
272 
writeInstitutions()273   void writeInstitutions()
274   {
275     Q_Q(MyMoneyStorageSql);
276     // first, get a list of what's on the database
277     // anything not in the list needs to be inserted
278     // anything which is will be updated and removed from the list
279     // anything left over at the end will need to be deleted
280     // this is an expensive and inconvenient way to do things; find a better way
281     // one way would be to build the lists when reading the db
282     // unfortunately this object does not persist between read and write
283     // it would also be nice if we could tell which objects had been updated since we read them in
284     QList<QString> dbList;
285     QSqlQuery query(*q);
286     query.prepare("SELECT id FROM kmmInstitutions;");
287     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Institution list"); // krazy:exclude=crashy
288     while (query.next()) dbList.append(query.value(0).toString());
289 
290     const QList<MyMoneyInstitution> list = m_storage->institutionList();
291     QList<MyMoneyInstitution> insertList;
292     QList<MyMoneyInstitution> updateList;
293     QSqlQuery query2(*q);
294     query.prepare(m_db.m_tables["kmmInstitutions"].updateString());
295     query2.prepare(m_db.m_tables["kmmInstitutions"].insertString());
296     signalProgress(0, list.count(), "Writing Institutions...");
297     foreach (const MyMoneyInstitution& i, list) {
298       if (dbList.contains(i.id())) {
299         dbList.removeAll(i.id());
300         updateList << i;
301       } else {
302         insertList << i;
303       }
304       signalProgress(++m_institutions, 0);
305     }
306     if (!insertList.isEmpty())
307       writeInstitutionList(insertList, query2);
308 
309     if (!updateList.isEmpty())
310       writeInstitutionList(updateList, query);
311 
312     if (!dbList.isEmpty()) {
313       QVariantList deleteList;
314       // qCopy segfaults here, so do it with a hand-rolled loop
315       foreach (const QString& it, dbList) {
316         deleteList << it;
317       }
318       query.prepare("DELETE FROM kmmInstitutions WHERE id = :id");
319       query.bindValue(":id", deleteList);
320       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Institution");
321 
322       deleteKeyValuePairs("INSTITUTION", deleteList);
323       // delete the old version in any case
324       deleteKeyValuePairs("OFXSETTINGS", deleteList);
325     }
326   }
327 
writePayees()328   void writePayees()
329   {
330     Q_Q(MyMoneyStorageSql);
331     // first, get a list of what's on the database (see writeInstitutions)
332 
333     QSqlQuery query(*q);
334     query.prepare("SELECT id FROM kmmPayees;");
335     if (!query.exec())
336       throw MYMONEYEXCEPTIONSQL("building Payee list"); // krazy:exclude=crashy
337 
338     QList<QString> dbList;
339     dbList.reserve(query.numRowsAffected());
340     while (query.next())
341       dbList.append(query.value(0).toString());
342 
343     QList<MyMoneyPayee> list = m_storage->payeeList();
344     MyMoneyPayee user(QString("USER"), m_storage->user());
345     list.prepend(user);
346     signalProgress(0, list.count(), "Writing Payees...");
347 
348     Q_FOREACH(const MyMoneyPayee& it, list) {
349       if (dbList.contains(it.id())) {
350         dbList.removeAll(it.id());
351         q->modifyPayee(it);
352       } else {
353         q->addPayee(it);
354       }
355       signalProgress(++m_payees, 0);
356     }
357 
358     if (!dbList.isEmpty()) {
359       QMap<QString, MyMoneyPayee> payeesToDelete = q->fetchPayees(dbList, true);
360       Q_FOREACH(const MyMoneyPayee& payee, payeesToDelete) {
361         q->removePayee(payee);
362       }
363     }
364   }
365 
writeTags()366   void writeTags()
367   {
368     Q_Q(MyMoneyStorageSql);
369     // first, get a list of what's on the database (see writeInstitutions)
370     QList<QString> dbList;
371     QSqlQuery query(*q);
372     query.prepare("SELECT id FROM kmmTags;");
373     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Tag list"); // krazy:exclude=crashy
374     while (query.next()) dbList.append(query.value(0).toString());
375 
376     QList<MyMoneyTag> list = m_storage->tagList();
377     signalProgress(0, list.count(), "Writing Tags...");
378     QSqlQuery query2(*q);
379     query.prepare(m_db.m_tables["kmmTags"].updateString());
380     query2.prepare(m_db.m_tables["kmmTags"].insertString());
381     foreach (const MyMoneyTag& it, list) {
382       if (dbList.contains(it.id())) {
383         dbList.removeAll(it.id());
384         writeTag(it, query);
385       } else {
386         writeTag(it, query2);
387       }
388       signalProgress(++m_tags, 0);
389     }
390 
391     if (!dbList.isEmpty()) {
392       QVariantList deleteList;
393       // qCopy segfaults here, so do it with a hand-rolled loop
394       foreach (const QString& it, dbList) {
395         deleteList << it;
396       }
397       query.prepare(m_db.m_tables["kmmTags"].deleteString());
398       query.bindValue(":id", deleteList);
399       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Tag");
400       m_tags -= query.numRowsAffected();
401     }
402   }
403 
writeAccounts()404   void writeAccounts()
405   {
406     Q_Q(MyMoneyStorageSql);
407     // first, get a list of what's on the database (see writeInstitutions)
408     QList<QString> dbList;
409     QSqlQuery query(*q);
410     query.prepare("SELECT id FROM kmmAccounts;");
411     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Account list"); // krazy:exclude=crashy
412     while (query.next()) dbList.append(query.value(0).toString());
413 
414     QList<MyMoneyAccount> list;
415     m_storage->accountList(list);
416     unsigned progress = 0;
417     signalProgress(0, list.count(), "Writing Accounts...");
418     if (dbList.isEmpty()) { // new table, insert standard accounts
419       query.prepare(m_db.m_tables["kmmAccounts"].insertString());
420     } else {
421       query.prepare(m_db.m_tables["kmmAccounts"].updateString());
422     }
423     // Attempt to write the standard accounts. For an empty db, this will fail.
424     try {
425       QList<MyMoneyAccount> stdList;
426       stdList << m_storage->asset();
427       stdList << m_storage->liability();
428       stdList << m_storage->expense();
429       stdList << m_storage->income();
430       stdList << m_storage->equity();
431       writeAccountList(stdList, query);
432       m_accounts += stdList.size();
433     } catch (const MyMoneyException &) {
434       // If the above failed, assume that the database is empty and create
435       // the standard accounts by hand before writing them.
436       MyMoneyAccount acc_l;
437       acc_l.setAccountType(Account::Type::Liability);
438       acc_l.setName("Liability");
439       MyMoneyAccount liability(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability), acc_l);
440 
441       MyMoneyAccount acc_a;
442       acc_a.setAccountType(Account::Type::Asset);
443       acc_a.setName("Asset");
444       MyMoneyAccount asset(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset), acc_a);
445 
446       MyMoneyAccount acc_e;
447       acc_e.setAccountType(Account::Type::Expense);
448       acc_e.setName("Expense");
449       MyMoneyAccount expense(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense), acc_e);
450 
451       MyMoneyAccount acc_i;
452       acc_i.setAccountType(Account::Type::Income);
453       acc_i.setName("Income");
454       MyMoneyAccount income(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income), acc_i);
455 
456       MyMoneyAccount acc_q;
457       acc_q.setAccountType(Account::Type::Equity);
458       acc_q.setName("Equity");
459       MyMoneyAccount equity(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity), acc_q);
460 
461       QList<MyMoneyAccount> stdList;
462       stdList << asset;
463       stdList << liability;
464       stdList << expense;
465       stdList << income;
466       stdList << equity;
467       writeAccountList(stdList, query);
468       m_accounts += stdList.size();
469     }
470 
471     QSqlQuery query2(*q);
472     query.prepare(m_db.m_tables["kmmAccounts"].updateString());
473     query2.prepare(m_db.m_tables["kmmAccounts"].insertString());
474     QList<MyMoneyAccount> updateList;
475     QList<MyMoneyAccount> insertList;
476     // Update the accounts that exist; insert the ones that do not.
477     foreach (const MyMoneyAccount& it, list) {
478       m_transactionCountMap[it.id()] = m_storage->transactionCount(it.id());
479       if (dbList.contains(it.id())) {
480         dbList.removeAll(it.id());
481         updateList << it;
482       } else {
483         insertList << it;
484       }
485       signalProgress(++progress, 0);
486       ++m_accounts;
487     }
488 
489     writeAccountList(updateList, query);
490     writeAccountList(insertList, query2);
491 
492     // Delete the accounts that are in the db but no longer in memory.
493     if (!dbList.isEmpty()) {
494       QVariantList kvpList;
495 
496       query.prepare("DELETE FROM kmmAccounts WHERE id = :id");
497       foreach (const QString& it, dbList) {
498         if (!m_storage->isStandardAccount(it)) {
499           kvpList << it;
500         }
501       }
502       query.bindValue(":id", kvpList);
503       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Account");
504 
505       deleteKeyValuePairs("ACCOUNT", kvpList);
506       deleteKeyValuePairs("ONLINEBANKING", kvpList);
507     }
508   }
509 
writeTransactions()510   void writeTransactions()
511   {
512     Q_Q(MyMoneyStorageSql);
513     // first, get a list of what's on the database (see writeInstitutions)
514     QList<QString> dbList;
515     QSqlQuery query(*q);
516     query.prepare("SELECT id FROM kmmTransactions WHERE txType = 'N';");
517     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Transaction list"); // krazy:exclude=crashy
518     while (query.next()) dbList.append(query.value(0).toString());
519 
520     MyMoneyTransactionFilter filter;
521     filter.setReportAllSplits(false);
522     QList<MyMoneyTransaction> list;
523     m_storage->transactionList(list, filter);
524     signalProgress(0, list.count(), "Writing Transactions...");
525     QSqlQuery q2(*q);
526     query.prepare(m_db.m_tables["kmmTransactions"].updateString());
527     q2.prepare(m_db.m_tables["kmmTransactions"].insertString());
528     foreach (const MyMoneyTransaction& it, list) {
529       if (dbList.contains(it.id())) {
530         dbList.removeAll(it.id());
531         writeTransaction(it.id(), it, query, "N");
532       } else {
533         writeTransaction(it.id(), it, q2, "N");
534       }
535       signalProgress(++m_transactions, 0);
536     }
537 
538     if (!dbList.isEmpty()) {
539       foreach (const QString& it, dbList) {
540         deleteTransaction(it);
541       }
542     }
543   }
544 
writeSchedules()545   void writeSchedules()
546   {
547     Q_Q(MyMoneyStorageSql);
548     // first, get a list of what's on the database (see writeInstitutions)
549     QList<QString> dbList;
550     QSqlQuery query(*q);
551     query.prepare("SELECT id FROM kmmSchedules;");
552     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Schedule list"); // krazy:exclude=crashy
553     while (query.next()) dbList.append(query.value(0).toString());
554 
555     const auto list = m_storage->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
556                                               QDate(), QDate(), false);
557     QSqlQuery query2(*q);
558     //TODO: find a way to prepare the queries outside of the loop.  writeSchedule()
559     // modifies the query passed to it, so they have to be re-prepared every pass.
560     signalProgress(0, list.count(), "Writing Schedules...");
561     foreach (const MyMoneySchedule& it, list) {
562       query.prepare(m_db.m_tables["kmmSchedules"].updateString());
563       query2.prepare(m_db.m_tables["kmmSchedules"].insertString());
564       bool insert = true;
565       if (dbList.contains(it.id())) {
566         dbList.removeAll(it.id());
567         insert = false;
568         writeSchedule(it, query, insert);
569       } else {
570         writeSchedule(it, query2, insert);
571       }
572       signalProgress(++m_schedules, 0);
573     }
574 
575     if (!dbList.isEmpty()) {
576       foreach (const QString& it, dbList) {
577         deleteSchedule(it);
578       }
579     }
580   }
581 
writeSecurities()582   void writeSecurities()
583   {
584     Q_Q(MyMoneyStorageSql);
585     // first, get a list of what's on the database (see writeInstitutions)
586     QList<QString> dbList;
587     QSqlQuery query(*q);
588     QSqlQuery query2(*q);
589     query.prepare("SELECT id FROM kmmSecurities;");
590     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building security list"); // krazy:exclude=crashy
591     while (query.next()) dbList.append(query.value(0).toString());
592 
593     const QList<MyMoneySecurity> securityList = m_storage->securityList();
594     signalProgress(0, securityList.count(), "Writing Securities...");
595     query.prepare(m_db.m_tables["kmmSecurities"].updateString());
596     query2.prepare(m_db.m_tables["kmmSecurities"].insertString());
597     foreach (const MyMoneySecurity& it, securityList) {
598       if (dbList.contains(it.id())) {
599         dbList.removeAll(it.id());
600         writeSecurity(it, query);
601       } else {
602         writeSecurity(it, query2);
603       }
604       signalProgress(++m_securities, 0);
605     }
606 
607     if (!dbList.isEmpty()) {
608       QVariantList idList;
609       // qCopy segfaults here, so do it with a hand-rolled loop
610       foreach (const QString& it, dbList) {
611         idList << it;
612       }
613 
614       query.prepare("DELETE FROM kmmSecurities WHERE id = :id");
615       query2.prepare("DELETE FROM kmmPrices WHERE fromId = :id OR toId = :id");
616       query.bindValue(":id", idList);
617       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security");
618 
619       query2.bindValue(":fromId", idList);
620       query2.bindValue(":toId", idList);
621       if (!query2.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security");
622 
623       deleteKeyValuePairs("SECURITY", idList);
624     }
625   }
626 
writePrices()627   void writePrices()
628   {
629     Q_Q(MyMoneyStorageSql);
630     // due to difficulties in matching and determining deletes
631     // easiest way is to delete all and re-insert
632     QSqlQuery query(*q);
633     query.prepare("DELETE FROM kmmPrices");
634     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Prices"); // krazy:exclude=crashy
635     m_prices = 0;
636 
637     const MyMoneyPriceList list = m_storage->priceList();
638     signalProgress(0, list.count(), "Writing Prices...");
639     MyMoneyPriceList::ConstIterator it;
640     for (it = list.constBegin(); it != list.constEnd(); ++it)   {
641       writePricePair(*it);
642     }
643   }
644 
writeCurrencies()645   void writeCurrencies()
646   {
647     Q_Q(MyMoneyStorageSql);
648     // first, get a list of what's on the database (see writeInstitutions)
649     QList<QString> dbList;
650     QSqlQuery query(*q);
651     QSqlQuery query2(*q);
652     query.prepare("SELECT ISOCode FROM kmmCurrencies;");
653     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Currency list"); // krazy:exclude=crashy
654     while (query.next()) dbList.append(query.value(0).toString());
655 
656     const QList<MyMoneySecurity> currencyList = m_storage->currencyList();
657     signalProgress(0, currencyList.count(), "Writing Currencies...");
658     query.prepare(m_db.m_tables["kmmCurrencies"].updateString());
659     query2.prepare(m_db.m_tables["kmmCurrencies"].insertString());
660     foreach (const MyMoneySecurity& it, currencyList) {
661       if (dbList.contains(it.id())) {
662         dbList.removeAll(it.id());
663         writeCurrency(it, query);
664       } else {
665         writeCurrency(it, query2);
666       }
667       signalProgress(++m_currencies, 0);
668     }
669 
670     if (!dbList.isEmpty()) {
671       QVariantList isoCodeList;
672       query.prepare("DELETE FROM kmmCurrencies WHERE ISOCode = :ISOCode");
673       // qCopy segfaults here, so do it with a hand-rolled loop
674       foreach (const QString& it, dbList) {
675         isoCodeList << it;
676       }
677 
678       query.bindValue(":ISOCode", isoCodeList);
679       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Currency");
680     }
681   }
682 
writeFileInfo()683   void writeFileInfo()
684   {
685     Q_Q(MyMoneyStorageSql);
686     // we have no real way of knowing when these change, so re-write them every time
687     QVariantList kvpList;
688     kvpList << "";
689     QList<QMap<QString, QString> > pairs;
690     pairs << m_storage->pairs();
691     deleteKeyValuePairs("STORAGE", kvpList);
692     writeKeyValuePairs("STORAGE", kvpList, pairs);
693 
694     QSqlQuery query(*q);
695     query.prepare("SELECT count(*) FROM kmmFileInfo;");
696     if (!query.exec() || !query.next())
697       throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy
698 
699     if (query.value(0).toInt() == 0) {
700       // Cannot use "INSERT INTO kmmFileInfo DEFAULT VALUES;" because it is not supported by MySQL
701       query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version) VALUES (null);"));
702       if (!query.exec()) throw MYMONEYEXCEPTIONSQL("inserting fileinfo"); // krazy:exclude=crashy
703     }
704 
705     query.prepare(QLatin1String(
706       "UPDATE kmmFileInfo SET "
707         "version = :version, "
708         "fixLevel = :fixLevel, "
709         "created = :created, "
710         "lastModified = :lastModified, "
711         "baseCurrency = :baseCurrency, "
712         "dateRangeStart = :dateRangeStart, "
713         "dateRangeEnd = :dateRangeEnd, "
714         "hiInstitutionId = :hiInstitutionId, "
715         "hiPayeeId = :hiPayeeId, "
716         "hiTagId = :hiTagId, "
717         "hiAccountId = :hiAccountId, "
718         "hiTransactionId = :hiTransactionId, "
719         "hiScheduleId = :hiScheduleId, "
720         "hiSecurityId = :hiSecurityId, "
721         "hiReportId = :hiReportId, "
722         "hiBudgetId = :hiBudgetId, "
723         "hiOnlineJobId = :hiOnlineJobId, "
724         "hiPayeeIdentifierId = :hiPayeeIdentifierId, "
725         "encryptData = :encryptData, "
726         "updateInProgress = :updateInProgress, "
727         "logonUser = :logonUser, "
728         "logonAt = :logonAt, "
729         //! @todo The following updates are for backwards compatibility only
730         //! remove backwards compatibility in a later version
731         "institutions = :institutions, "
732         "accounts = :accounts, "
733         "payees = :payees, "
734         "tags = :tags, "
735         "transactions = :transactions, "
736         "splits = :splits, "
737         "securities = :securities, "
738         "prices = :prices, "
739         "currencies = :currencies, "
740         "schedules = :schedules, "
741         "reports = :reports, "
742         "kvps = :kvps, "
743         "budgets = :budgets; "
744       )
745     );
746 
747     query.bindValue(":version", m_dbVersion);
748     query.bindValue(":fixLevel", m_storage->fileFixVersion());
749     query.bindValue(":created", m_storage->creationDate().toString(Qt::ISODate));
750     //q.bindValue(":lastModified", m_storage->lastModificationDate().toString(Qt::ISODate));
751     query.bindValue(":lastModified", QDate::currentDate().toString(Qt::ISODate));
752     query.bindValue(":baseCurrency", m_storage->pairs()["kmm-baseCurrency"]);
753     query.bindValue(":dateRangeStart", QDate());
754     query.bindValue(":dateRangeEnd", QDate());
755 
756     //FIXME: This modifies all m_<variable> used in this function.
757     // Sometimes the memory has been updated.
758 
759     // Should most of these be tracked in a view?
760     // Variables actually needed are: version, fileFixVersion, creationDate,
761     // baseCurrency, encryption, update info, and logon info.
762     //try {
763     //readFileInfo();
764     //} catch (...) {
765     //q->startCommitUnit(Q_FUNC_INFO);
766     //}
767 
768     //! @todo The following bindings are for backwards compatibility only
769     //! remove backwards compatibility in a later version
770     query.bindValue(":hiInstitutionId", QVariant::fromValue(q->getNextInstitutionId()));
771     query.bindValue(":hiPayeeId", QVariant::fromValue(q->getNextPayeeId()));
772     query.bindValue(":hiTagId", QVariant::fromValue(q->getNextTagId()));
773     query.bindValue(":hiAccountId", QVariant::fromValue(q->getNextAccountId()));
774     query.bindValue(":hiTransactionId", QVariant::fromValue(q->getNextTransactionId()));
775     query.bindValue(":hiScheduleId", QVariant::fromValue(q->getNextScheduleId()));
776     query.bindValue(":hiSecurityId", QVariant::fromValue(q->getNextSecurityId()));
777     query.bindValue(":hiReportId", QVariant::fromValue(q->getNextReportId()));
778     query.bindValue(":hiBudgetId", QVariant::fromValue(q->getNextBudgetId()));
779     query.bindValue(":hiOnlineJobId", QVariant::fromValue(q->getNextOnlineJobId()));
780     query.bindValue(":hiPayeeIdentifierId", QVariant::fromValue(q->getNextPayeeIdentifierId()));
781 
782     query.bindValue(":encryptData", m_encryptData);
783     query.bindValue(":updateInProgress", "N");
784     query.bindValue(":logonUser", m_logonUser);
785     query.bindValue(":logonAt", m_logonAt.toString(Qt::ISODate));
786 
787     //! @todo The following bindings are for backwards compatibility only
788     //! remove backwards compatibility in a later version
789     query.bindValue(":institutions", (unsigned long long) m_institutions);
790     query.bindValue(":accounts", (unsigned long long) m_accounts);
791     query.bindValue(":payees", (unsigned long long) m_payees);
792     query.bindValue(":tags", (unsigned long long) m_tags);
793     query.bindValue(":transactions", (unsigned long long) m_transactions);
794     query.bindValue(":splits", (unsigned long long) m_splits);
795     query.bindValue(":securities", (unsigned long long) m_securities);
796     query.bindValue(":prices", (unsigned long long) m_prices);
797     query.bindValue(":currencies", (unsigned long long) m_currencies);
798     query.bindValue(":schedules", (unsigned long long) m_schedules);
799     query.bindValue(":reports", (unsigned long long) m_reports);
800     query.bindValue(":kvps", (unsigned long long) m_kvps);
801     query.bindValue(":budgets", (unsigned long long) m_budgets);
802 
803     if (!query.exec())
804       throw MYMONEYEXCEPTIONSQL("writing FileInfo"); // krazy:exclude=crashy
805   }
806 
writeReports()807   void writeReports()
808   {
809     Q_Q(MyMoneyStorageSql);
810     // first, get a list of what's on the database (see writeInstitutions)
811     QList<QString> dbList;
812     QSqlQuery query(*q);
813     QSqlQuery query2(*q);
814     query.prepare("SELECT id FROM kmmReportConfig;");
815     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Report list"); // krazy:exclude=crashy
816     while (query.next()) dbList.append(query.value(0).toString());
817 
818     QList<MyMoneyReport> list = m_storage->reportList();
819     signalProgress(0, list.count(), "Writing Reports...");
820     query.prepare(m_db.m_tables["kmmReportConfig"].updateString());
821     query2.prepare(m_db.m_tables["kmmReportConfig"].insertString());
822     foreach (const MyMoneyReport& it, list) {
823       if (dbList.contains(it.id())) {
824         dbList.removeAll(it.id());
825         writeReport(it, query);
826       } else {
827         writeReport(it, query2);
828       }
829       signalProgress(++m_reports, 0);
830     }
831 
832     if (!dbList.isEmpty()) {
833       QVariantList idList;
834       query.prepare("DELETE FROM kmmReportConfig WHERE id = :id");
835       // qCopy segfaults here, so do it with a hand-rolled loop
836       foreach (const QString& it, dbList) {
837         idList << it;
838       }
839 
840       query.bindValue(":id", idList);
841       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Report");
842     }
843   }
844 
writeBudgets()845   void writeBudgets()
846   {
847     Q_Q(MyMoneyStorageSql);
848     // first, get a list of what's on the database (see writeInstitutions)
849     QList<QString> dbList;
850     QSqlQuery query(*q);
851     QSqlQuery query2(*q);
852     query.prepare("SELECT name FROM kmmBudgetConfig;");
853     if (!query.exec())
854       throw MYMONEYEXCEPTIONSQL("building Budget list"); // krazy:exclude=crashy
855     while (query.next())
856       dbList.append(query.value(0).toString());
857 
858     QList<MyMoneyBudget> list = m_storage->budgetList();
859     signalProgress(0, list.count(), "Writing Budgets...");
860     query.prepare(m_db.m_tables["kmmBudgetConfig"].updateString());
861     query2.prepare(m_db.m_tables["kmmBudgetConfig"].insertString());
862     foreach (const MyMoneyBudget& it, list) {
863       if (dbList.contains(it.name())) {
864         dbList.removeAll(it.name());
865         writeBudget(it, query);
866       } else {
867         writeBudget(it, query2);
868       }
869       signalProgress(++m_budgets, 0);
870     }
871 
872     if (!dbList.isEmpty()) {
873       QVariantList idList;
874       query.prepare("DELETE FROM kmmBudgetConfig WHERE id = :id");
875       // qCopy segfaults here, so do it with a hand-rolled loop
876       foreach (const QString& it, dbList) {
877         idList << it;
878       }
879 
880       query.bindValue(":name", idList);
881       if (!query.execBatch())
882         throw MYMONEYEXCEPTIONSQL("deleting Budget");
883     }
884   }
885 
clearTable(const QString & tableName,QSqlQuery & query)886   bool clearTable(const QString& tableName, QSqlQuery& query)
887   {
888     if (query.exec(QString("SELECT count(*) FROM %1").arg(tableName))) {
889       if (query.next()) {
890         if (query.value(0).toUInt() > 0) {
891           if (!query.exec(QString("DELETE FROM %1").arg(tableName)))
892             return false;
893         }
894       }
895     }
896     return true;
897   }
898 
writeOnlineJobs()899   void writeOnlineJobs()
900   {
901     Q_Q(MyMoneyStorageSql);
902     QSqlQuery query(*q);
903     if (!clearTable(QStringLiteral("kmmOnlineJobs"), query))
904       throw MYMONEYEXCEPTIONSQL("Clean kmmOnlineJobs table");
905 
906     if (!clearTable(QStringLiteral("kmmSepaOrders"), query))
907       throw MYMONEYEXCEPTIONSQL("Clean kmmSepaOrders table");
908 
909     if (!clearTable(QStringLiteral("kmmNationalAccountNumber"), query))
910       throw MYMONEYEXCEPTIONSQL("Clean kmmNationalAccountNumber table");
911 
912     const QList<onlineJob> jobs(m_storage->onlineJobList());
913     signalProgress(0, jobs.count(), i18n("Inserting online jobs."));
914     // Create list for onlineJobs which failed and the reason therefor
915     QList<QPair<onlineJob, QString> > failedJobs;
916     int jobCount = 0;
917     foreach (const onlineJob& job, jobs) {
918       try {
919         q->addOnlineJob(job);
920       } catch (const MyMoneyException &e) {
921         // Do not save e as this may point to an inherited class
922         failedJobs.append(QPair<onlineJob, QString>(job, e.what()));
923         qDebug() << "Failed to save onlineJob" << job.id() << "Reason:" << e.what();
924       }
925 
926       signalProgress(++jobCount, 0);
927     }
928 
929     if (!failedJobs.isEmpty()) {
930       /** @todo Improve error message */
931       throw MYMONEYEXCEPTION_CSTRING("Could not save onlineJob.");
932     }
933   }
934   /** @} */
935 
936   /**
937    * @name writeMethods
938    * @{
939    * These methods bind the data fields of MyMoneyObjects to a given query and execute the query.
940    * This is helpful as the query has usually an update and a insert format.
941    */
writeInstitutionList(const QList<MyMoneyInstitution> & iList,QSqlQuery & query)942   void writeInstitutionList(const QList<MyMoneyInstitution>& iList, QSqlQuery& query)
943   {
944     QVariantList idList;
945     QVariantList nameList;
946     QVariantList managerList;
947     QVariantList routingCodeList;
948     QVariantList addressStreetList;
949     QVariantList addressCityList;
950     QVariantList addressZipcodeList;
951     QVariantList telephoneList;
952     QList<QMap<QString, QString> > kvpPairsList;
953 
954     foreach (const MyMoneyInstitution& i, iList) {
955       idList << i.id();
956       nameList << i.name();
957       managerList << i.manager();
958       routingCodeList << i.sortcode();
959       addressStreetList << i.street();
960       addressCityList << i.city();
961       addressZipcodeList << i.postcode();
962       telephoneList << i.telephone();
963       kvpPairsList << i.pairs();
964     }
965 
966     query.bindValue(":id", idList);
967     query.bindValue(":name", nameList);
968     query.bindValue(":manager", managerList);
969     query.bindValue(":routingCode", routingCodeList);
970     query.bindValue(":addressStreet", addressStreetList);
971     query.bindValue(":addressCity", addressCityList);
972     query.bindValue(":addressZipcode", addressZipcodeList);
973     query.bindValue(":telephone", telephoneList);
974 
975     if (!query.execBatch())
976       throw MYMONEYEXCEPTIONSQL("writing Institution");
977     deleteKeyValuePairs("INSTITUTION", idList);
978     // delete the old version in any case
979     deleteKeyValuePairs("OFXSETTINGS", idList);
980     writeKeyValuePairs("INSTITUTION", idList, kvpPairsList);
981     // Set m_hiIdInstitutions to 0 to force recalculation the next time it is requested
982     m_hiIdInstitutions = 0;
983   }
984 
985   void writePayee(const MyMoneyPayee& p, QSqlQuery& query, bool isUserInfo = false)
986   {
987     if (isUserInfo) {
988       query.bindValue(":id", "USER");
989     } else {
990       query.bindValue(":id", p.id());
991     }
992     query.bindValue(":name", p.name());
993     query.bindValue(":reference", p.reference());
994     query.bindValue(":email", p.email());
995     query.bindValue(":addressStreet", p.address());
996     query.bindValue(":addressCity", p.city());
997     query.bindValue(":addressZipcode", p.postcode());
998     query.bindValue(":addressState", p.state());
999     query.bindValue(":telephone", p.telephone());
1000     query.bindValue(":notes", p.notes());
1001     query.bindValue(":defaultAccountId", p.defaultAccountId());
1002     bool ignoreCase;
1003     QString matchKeys;
1004     auto type = p.matchData(ignoreCase, matchKeys);
1005     query.bindValue(":matchData", static_cast<uint>(type));
1006 
1007     if (ignoreCase)
1008       query.bindValue(":matchIgnoreCase", "Y");
1009     else
1010       query.bindValue(":matchIgnoreCase", "N");
1011 
1012     query.bindValue(":matchKeys", matchKeys);
1013     if (!query.exec()) // krazy:exclude=crashy
1014       throw MYMONEYEXCEPTIONSQL("writing Payee"); // krazy:exclude=crashy
1015 
1016     if (!isUserInfo)
1017       m_hiIdPayees = 0;
1018   }
1019 
writeTag(const MyMoneyTag & ta,QSqlQuery & query)1020   void writeTag(const MyMoneyTag& ta, QSqlQuery& query)
1021   {
1022     query.bindValue(":id", ta.id());
1023     query.bindValue(":name", ta.name());
1024     query.bindValue(":tagColor", ta.tagColor().name());
1025     if (ta.isClosed()) query.bindValue(":closed", "Y");
1026     else query.bindValue(":closed", "N");
1027     query.bindValue(":notes", ta.notes());
1028     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Tag"); // krazy:exclude=crashy
1029     m_hiIdTags = 0;
1030   }
1031 
writeAccountList(const QList<MyMoneyAccount> & accList,QSqlQuery & query)1032   void writeAccountList(const QList<MyMoneyAccount>& accList, QSqlQuery& query)
1033   {
1034     //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate());
1035 
1036     QVariantList idList;
1037     QVariantList institutionIdList;
1038     QVariantList parentIdList;
1039     QVariantList lastReconciledList;
1040     QVariantList lastModifiedList;
1041     QVariantList openingDateList;
1042     QVariantList accountNumberList;
1043     QVariantList accountTypeList;
1044     QVariantList accountTypeStringList;
1045     QVariantList isStockAccountList;
1046     QVariantList accountNameList;
1047     QVariantList descriptionList;
1048     QVariantList currencyIdList;
1049     QVariantList balanceList;
1050     QVariantList balanceFormattedList;
1051     QVariantList transactionCountList;
1052 
1053     QList<QMap<QString, QString> > pairs;
1054     QList<QMap<QString, QString> > onlineBankingPairs;
1055 
1056     foreach (const MyMoneyAccount& a, accList) {
1057       idList << a.id();
1058       institutionIdList << a.institutionId();
1059       parentIdList << a.parentAccountId();
1060       if (a.lastReconciliationDate() == QDate())
1061         lastReconciledList << a.lastReconciliationDate();
1062       else
1063         lastReconciledList << a.lastReconciliationDate().toString(Qt::ISODate);
1064       lastModifiedList << a.lastModified();
1065       if (a.openingDate() == QDate())
1066         openingDateList << a.openingDate();
1067       else
1068         openingDateList << a.openingDate().toString(Qt::ISODate);
1069       accountNumberList << a.number();
1070       accountTypeList << (int)a.accountType();
1071       accountTypeStringList << MyMoneyAccount::accountTypeToString(a.accountType());
1072       if (a.accountType() == Account::Type::Stock)
1073         isStockAccountList << "Y";
1074       else
1075         isStockAccountList << "N";
1076       accountNameList << a.name();
1077       descriptionList << a.description();
1078       currencyIdList << a.currencyId();
1079       // This section attempts to get the balance from the database, if possible
1080       // That way, the balance fields are kept in sync. If that fails, then
1081       // It is assumed that the account actually knows its correct balance.
1082 
1083       //FIXME: Using exceptions for branching always feels like a kludge.
1084       //       Look for a better way.
1085       try {
1086         MyMoneyMoney bal = m_storage->balance(a.id(), QDate());
1087         balanceList << bal.toString();
1088         balanceFormattedList << bal.formatMoney("", -1, false);
1089       } catch (const MyMoneyException &) {
1090         balanceList << a.balance().toString();
1091         balanceFormattedList << a.balance().formatMoney("", -1, false);
1092       }
1093       transactionCountList << quint64(m_transactionCountMap[a.id()]);
1094 
1095       //MMAccount inherits from KVPContainer AND has a KVPContainer member
1096       //so handle both
1097       pairs << a.pairs();
1098       onlineBankingPairs << a.onlineBankingSettings().pairs();
1099     }
1100 
1101     query.bindValue(":id", idList);
1102     query.bindValue(":institutionId", institutionIdList);
1103     query.bindValue(":parentId", parentIdList);
1104     query.bindValue(":lastReconciled", lastReconciledList);
1105     query.bindValue(":lastModified", lastModifiedList);
1106     query.bindValue(":openingDate", openingDateList);
1107     query.bindValue(":accountNumber", accountNumberList);
1108     query.bindValue(":accountType", accountTypeList);
1109     query.bindValue(":accountTypeString", accountTypeStringList);
1110     query.bindValue(":isStockAccount", isStockAccountList);
1111     query.bindValue(":accountName", accountNameList);
1112     query.bindValue(":description", descriptionList);
1113     query.bindValue(":currencyId", currencyIdList);
1114     query.bindValue(":balance", balanceList);
1115     query.bindValue(":balanceFormatted", balanceFormattedList);
1116     query.bindValue(":transactionCount", transactionCountList);
1117 
1118     if (!query.execBatch())
1119       throw MYMONEYEXCEPTIONSQL("writing Account");
1120 
1121     //Add in Key-Value Pairs for accounts.
1122     deleteKeyValuePairs("ACCOUNT", idList);
1123     deleteKeyValuePairs("ONLINEBANKING", idList);
1124     writeKeyValuePairs("ACCOUNT", idList, pairs);
1125     writeKeyValuePairs("ONLINEBANKING", idList, onlineBankingPairs);
1126     m_hiIdAccounts = 0;
1127   }
1128 
writeTransaction(const QString & txId,const MyMoneyTransaction & tx,QSqlQuery & query,const QString & type)1129   void writeTransaction(const QString& txId, const MyMoneyTransaction& tx, QSqlQuery& query, const QString& type)
1130   {
1131     query.bindValue(":id", txId);
1132     query.bindValue(":txType", type);
1133     query.bindValue(":postDate", tx.postDate().toString(Qt::ISODate));
1134     query.bindValue(":memo", tx.memo());
1135     query.bindValue(":entryDate", tx.entryDate().toString(Qt::ISODate));
1136     query.bindValue(":currencyId", tx.commodity());
1137     query.bindValue(":bankId", tx.bankID());
1138 
1139     if (!query.exec()) // krazy:exclude=crashy
1140       throw MYMONEYEXCEPTIONSQL("writing Transaction"); // krazy:exclude=crashy
1141 
1142     m_txPostDate = tx.postDate(); // FIXME: TEMP till Tom puts date in split object
1143     QList<MyMoneySplit> splitList = tx.splits();
1144     writeSplits(txId, type, splitList);
1145 
1146     //Add in Key-Value Pairs for transactions.
1147     QVariantList idList;
1148     idList << txId;
1149     deleteKeyValuePairs("TRANSACTION", idList);
1150     QList<QMap<QString, QString> > pairs;
1151     pairs << tx.pairs();
1152     writeKeyValuePairs("TRANSACTION", idList, pairs);
1153     m_hiIdTransactions = 0;
1154   }
1155 
writeSplits(const QString & txId,const QString & type,const QList<MyMoneySplit> & splitList)1156   void writeSplits(const QString& txId, const QString& type, const QList<MyMoneySplit>& splitList)
1157   {
1158     Q_Q(MyMoneyStorageSql);
1159     // first, get a list of what's on the database (see writeInstitutions)
1160     QList<uint> dbList;
1161     QList<MyMoneySplit> insertList;
1162     QList<MyMoneySplit> updateList;
1163     QList<int> insertIdList;
1164     QList<int> updateIdList;
1165     QSqlQuery query(*q);
1166     query.prepare("SELECT splitId FROM kmmSplits where transactionId = :id;");
1167     query.bindValue(":id", txId);
1168     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Split list"); // krazy:exclude=crashy
1169     while (query.next()) dbList.append(query.value(0).toUInt());
1170 
1171     QSqlQuery query2(*q);
1172     query.prepare(m_db.m_tables["kmmSplits"].updateString());
1173     query2.prepare(m_db.m_tables["kmmSplits"].insertString());
1174     auto i = 0;
1175     for (auto it = splitList.constBegin(); it != splitList.constEnd(); ++it) {
1176       if (dbList.contains(i)) {
1177         dbList.removeAll(i);
1178         updateList << *it;
1179         updateIdList << i;
1180       } else {
1181         ++m_splits;
1182         insertList << *it;
1183         insertIdList << i;
1184       }
1185       ++i;
1186     }
1187 
1188     if (!insertList.isEmpty()) {
1189       writeSplitList(txId, insertList, type, insertIdList, query2);
1190       writeTagSplitsList(txId, insertList, insertIdList);
1191     }
1192 
1193     if (!updateList.isEmpty()) {
1194       writeSplitList(txId, updateList, type, updateIdList, query);
1195       deleteTagSplitsList(txId, updateIdList);
1196       writeTagSplitsList(txId, updateList, updateIdList);
1197     }
1198 
1199     if (!dbList.isEmpty()) {
1200       QVector<QVariant> txIdList(dbList.count(), txId);
1201       QVariantList splitIdList;
1202       query.prepare("DELETE FROM kmmSplits WHERE transactionId = :txId AND splitId = :splitId");
1203       // qCopy segfaults here, so do it with a hand-rolled loop
1204       foreach (int it, dbList) {
1205         splitIdList << it;
1206       }
1207       query.bindValue(":txId", txIdList.toList());
1208       query.bindValue(":splitId", splitIdList);
1209       if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits");
1210     }
1211   }
1212 
writeTagSplitsList(const QString & txId,const QList<MyMoneySplit> & splitList,const QList<int> & splitIdList)1213   void writeTagSplitsList
1214   (const QString& txId,
1215    const QList<MyMoneySplit>& splitList,
1216    const QList<int>& splitIdList)
1217   {
1218     Q_Q(MyMoneyStorageSql);
1219     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
1220     QVariantList tagIdList;
1221     QVariantList txIdList;
1222     QVariantList splitIdList_TagSplits;
1223 
1224     int i = 0, l = 0;
1225     foreach (const MyMoneySplit& s, splitList) {
1226       for (l = 0; l < s.tagIdList().size(); ++l) {
1227         tagIdList << s.tagIdList()[l];
1228         splitIdList_TagSplits << splitIdList[i];
1229         txIdList << txId;
1230       }
1231       i++;
1232     }
1233     QSqlQuery query(*q);
1234     query.prepare(m_db.m_tables["kmmTagSplits"].insertString());
1235     query.bindValue(":tagId", tagIdList);
1236     query.bindValue(":splitId", splitIdList_TagSplits);
1237     query.bindValue(":transactionId", txIdList);
1238     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing tagSplits");
1239   }
1240 
writeSplitList(const QString & txId,const QList<MyMoneySplit> & splitList,const QString & type,const QList<int> & splitIdList,QSqlQuery & query)1241   void writeSplitList
1242   (const QString& txId,
1243    const QList<MyMoneySplit>& splitList,
1244    const QString& type,
1245    const QList<int>& splitIdList,
1246    QSqlQuery& query)
1247   {
1248     QVariantList txIdList;
1249     QVariantList typeList;
1250     QVariantList payeeIdList;
1251     QVariantList reconcileDateList;
1252     QVariantList actionList;
1253     QVariantList reconcileFlagList;
1254     QVariantList valueList;
1255     QVariantList valueFormattedList;
1256     QVariantList sharesList;
1257     QVariantList sharesFormattedList;
1258     QVariantList priceList;
1259     QVariantList priceFormattedList;
1260     QVariantList memoList;
1261     QVariantList accountIdList;
1262     QVariantList costCenterIdList;
1263     QVariantList checkNumberList;
1264     QVariantList postDateList;
1265     QVariantList bankIdList;
1266     QVariantList kvpIdList;
1267     QList<QMap<QString, QString> > kvpPairsList;
1268 
1269     int i = 0;
1270     foreach (const MyMoneySplit& s, splitList) {
1271       txIdList << txId;
1272       typeList << type;
1273       payeeIdList << s.payeeId();
1274       if (s.reconcileDate() == QDate())
1275         reconcileDateList << s.reconcileDate();
1276       else
1277         reconcileDateList << s.reconcileDate().toString(Qt::ISODate);
1278       actionList << s.action();
1279       reconcileFlagList << (int)s.reconcileFlag();
1280       valueList << s.value().toString();
1281       valueFormattedList << s.value().formatMoney("", -1, false).replace(QChar(','), QChar('.'));
1282       sharesList << s.shares().toString();
1283       MyMoneyAccount acc = m_storage->account(s.accountId());
1284       MyMoneySecurity sec = m_storage->security(acc.currencyId());
1285       sharesFormattedList << s.price().
1286       formatMoney("", MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()), false).
1287       replace(QChar(','), QChar('.'));
1288       MyMoneyMoney price = s.actualPrice();
1289       if (!price.isZero()) {
1290         priceList << price.toString();
1291         priceFormattedList << price.formatMoney
1292         ("", sec.pricePrecision(), false)
1293         .replace(QChar(','), QChar('.'));
1294       } else {
1295         priceList << QString();
1296         priceFormattedList << QString();
1297       }
1298       memoList << s.memo();
1299       accountIdList << s.accountId();
1300       costCenterIdList << s.costCenterId();
1301       checkNumberList << s.number();
1302       postDateList << m_txPostDate.toString(Qt::ISODate); // FIXME: when Tom puts date into split object
1303       bankIdList << s.bankID();
1304 
1305       kvpIdList << QString(txId + QString::number(splitIdList[i]));
1306       kvpPairsList << s.pairs();
1307       ++i;
1308     }
1309 
1310     query.bindValue(":transactionId", txIdList);
1311     query.bindValue(":txType", typeList);
1312     QVariantList iList;
1313     // qCopy segfaults here, so do it with a hand-rolled loop
1314     foreach (int it_s, splitIdList) {
1315       iList << it_s;
1316     }
1317 
1318     query.bindValue(":splitId", iList);
1319     query.bindValue(":payeeId", payeeIdList);
1320     query.bindValue(":reconcileDate", reconcileDateList);
1321     query.bindValue(":action", actionList);
1322     query.bindValue(":reconcileFlag", reconcileFlagList);
1323     query.bindValue(":value", valueList);
1324     query.bindValue(":valueFormatted", valueFormattedList);
1325     query.bindValue(":shares", sharesList);
1326     query.bindValue(":sharesFormatted", sharesFormattedList);
1327     query.bindValue(":price", priceList);
1328     query.bindValue(":priceFormatted", priceFormattedList);
1329     query.bindValue(":memo", memoList);
1330     query.bindValue(":accountId", accountIdList);
1331     query.bindValue(":costCenterId", costCenterIdList);
1332     query.bindValue(":checkNumber", checkNumberList);
1333     query.bindValue(":postDate", postDateList);
1334     query.bindValue(":bankId", bankIdList);
1335     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Split");
1336     deleteKeyValuePairs("SPLIT", kvpIdList);
1337     writeKeyValuePairs("SPLIT", kvpIdList, kvpPairsList);
1338   }
1339 
writeSchedule(const MyMoneySchedule & sch,QSqlQuery & query,bool insert)1340   void writeSchedule(const MyMoneySchedule& sch, QSqlQuery& query, bool insert)
1341   {
1342     query.bindValue(":id", sch.id());
1343     query.bindValue(":name", sch.name());
1344     query.bindValue(":type", (int)sch.type());
1345     query.bindValue(":typeString", MyMoneySchedule::scheduleTypeToString(sch.type()));
1346     query.bindValue(":occurence", (int)sch.occurrence()); // krazy:exclude=spelling
1347     query.bindValue(":occurenceMultiplier", sch.occurrenceMultiplier()); // krazy:exclude=spelling
1348     query.bindValue(":occurenceString", sch.occurrenceToString()); // krazy:exclude=spelling
1349     query.bindValue(":paymentType", (int)sch.paymentType());
1350     query.bindValue(":paymentTypeString", MyMoneySchedule::paymentMethodToString(sch.paymentType()));
1351     query.bindValue(":startDate", sch.startDate().toString(Qt::ISODate));
1352     query.bindValue(":endDate", sch.endDate().toString(Qt::ISODate));
1353     if (sch.isFixed()) {
1354       query.bindValue(":fixed", "Y");
1355     } else {
1356       query.bindValue(":fixed", "N");
1357     }
1358     if (sch.lastDayInMonth()) {
1359       query.bindValue(":lastDayInMonth", "Y");
1360     } else {
1361       query.bindValue(":lastDayInMonth", "N");
1362     }
1363     if (sch.autoEnter()) {
1364       query.bindValue(":autoEnter", "Y");
1365     } else {
1366       query.bindValue(":autoEnter", "N");
1367     }
1368     query.bindValue(":lastPayment", sch.lastPayment());
1369     query.bindValue(":nextPaymentDue", sch.nextDueDate().toString(Qt::ISODate));
1370     query.bindValue(":weekendOption", (int)sch.weekendOption());
1371     query.bindValue(":weekendOptionString", MyMoneySchedule::weekendOptionToString(sch.weekendOption()));
1372     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedules"); // krazy:exclude=crashy
1373 
1374     //store the payment history for this scheduled task.
1375     //easiest way is to delete all and re-insert; it's not a high use table
1376     query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id;");
1377     query.bindValue(":id", sch.id());
1378     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting  Schedule Payment History"); // krazy:exclude=crashy
1379 
1380     query.prepare(m_db.m_tables["kmmSchedulePaymentHistory"].insertString());
1381     foreach (const QDate& it, sch.recordedPayments()) {
1382       query.bindValue(":schedId", sch.id());
1383       query.bindValue(":payDate", it.toString(Qt::ISODate));
1384       if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedule Payment History"); // krazy:exclude=crashy
1385     }
1386 
1387     //store the transaction data for this task.
1388     if (!insert) {
1389       query.prepare(m_db.m_tables["kmmTransactions"].updateString());
1390     } else {
1391       query.prepare(m_db.m_tables["kmmTransactions"].insertString());
1392     }
1393     writeTransaction(sch.id(), sch.transaction(), query, "S");
1394 
1395     //FIXME: enable when schedules have KVPs.
1396 
1397     //Add in Key-Value Pairs for transactions.
1398     //deleteKeyValuePairs("SCHEDULE", sch.id());
1399     //writeKeyValuePairs("SCHEDULE", sch.id(), sch.pairs());
1400   }
1401 
writeSecurity(const MyMoneySecurity & security,QSqlQuery & query)1402   void writeSecurity(const MyMoneySecurity& security, QSqlQuery& query)
1403   {
1404     query.bindValue(":id", security.id());
1405     query.bindValue(":name", security.name());
1406     query.bindValue(":symbol", security.tradingSymbol());
1407     query.bindValue(":type", static_cast<int>(security.securityType()));
1408     query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType()));
1409     query.bindValue(":roundingMethod", static_cast<int>(security.roundingMethod()));
1410     query.bindValue(":smallestAccountFraction", security.smallestAccountFraction());
1411     query.bindValue(":pricePrecision", security.pricePrecision());
1412     query.bindValue(":tradingCurrency", security.tradingCurrency());
1413     query.bindValue(":tradingMarket", security.tradingMarket());
1414     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Securities"); // krazy:exclude=crashy
1415 
1416     //Add in Key-Value Pairs for security
1417     QVariantList idList;
1418     idList << security.id();
1419     QList<QMap<QString, QString> > pairs;
1420     pairs << security.pairs();
1421     deleteKeyValuePairs("SECURITY", idList);
1422     writeKeyValuePairs("SECURITY", idList, pairs);
1423     m_hiIdSecurities = 0;
1424   }
1425 
writePricePair(const MyMoneyPriceEntries & p)1426   void writePricePair(const MyMoneyPriceEntries& p)
1427   {
1428     MyMoneyPriceEntries::ConstIterator it;
1429     for (it = p.constBegin(); it != p.constEnd(); ++it) {
1430       writePrice(*it);
1431       signalProgress(++m_prices, 0);
1432     }
1433   }
1434 
writePrice(const MyMoneyPrice & p)1435   void writePrice(const MyMoneyPrice& p)
1436   {
1437     Q_Q(MyMoneyStorageSql);
1438     QSqlQuery query(*q);
1439     query.prepare(m_db.m_tables["kmmPrices"].insertString());
1440     query.bindValue(":fromId", p.from());
1441     query.bindValue(":toId", p.to());
1442     query.bindValue(":priceDate", p.date().toString(Qt::ISODate));
1443     query.bindValue(":price", p.rate(QString()).toString());
1444     query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", 2));
1445     query.bindValue(":priceSource", p.source());
1446     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Prices"); // krazy:exclude=crashy
1447   }
1448 
writeCurrency(const MyMoneySecurity & currency,QSqlQuery & query)1449   void writeCurrency(const MyMoneySecurity& currency, QSqlQuery& query)
1450   {
1451     query.bindValue(":ISOcode", currency.id());
1452     query.bindValue(":name", currency.name());
1453     query.bindValue(":type", static_cast<int>(currency.securityType()));
1454     query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(currency.securityType()));
1455     // writing the symbol as three short ints is a PITA, but the
1456     // problem is that database drivers have incompatible ways of declaring UTF8
1457     QString symbol = currency.tradingSymbol() + "   ";
1458     const ushort* symutf = symbol.utf16();
1459     //int ix = 0;
1460     //while (x[ix] != '\0') qDebug() << "symbol" << symbol << "char" << ix << "=" << x[ix++];
1461     //q.bindValue(":symbol1", symbol.mid(0,1).unicode()->unicode());
1462     //q.bindValue(":symbol2", symbol.mid(1,1).unicode()->unicode());
1463     //q.bindValue(":symbol3", symbol.mid(2,1).unicode()->unicode());
1464     query.bindValue(":symbol1", symutf[0]);
1465     query.bindValue(":symbol2", symutf[1]);
1466     query.bindValue(":symbol3", symutf[2]);
1467     query.bindValue(":symbolString", symbol);
1468     query.bindValue(":smallestCashFraction", currency.smallestCashFraction());
1469     query.bindValue(":smallestAccountFraction", currency.smallestAccountFraction());
1470     query.bindValue(":pricePrecision", currency.pricePrecision());
1471     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Currencies"); // krazy:exclude=crashy
1472   }
1473 
writeReport(const MyMoneyReport & rep,QSqlQuery & query)1474   void writeReport(const MyMoneyReport& rep, QSqlQuery& query)
1475   {
1476     QDomDocument d; // create a dummy XML document
1477     QDomElement e = d.createElement("REPORTS");
1478     d.appendChild(e);
1479     MyMoneyXmlContentHandler2::writeReport(rep, d, e); // write the XML to document
1480     query.bindValue(":id", rep.id());
1481     query.bindValue(":name", rep.name());
1482     query.bindValue(":XML", d.toString());
1483     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Reports"); // krazy:exclude=crashy
1484   }
1485 
writeBudget(const MyMoneyBudget & bud,QSqlQuery & query)1486   void writeBudget(const MyMoneyBudget& bud, QSqlQuery& query)
1487   {
1488     QDomDocument d; // create a dummy XML document
1489     QDomElement e = d.createElement("BUDGETS");
1490     d.appendChild(e);
1491     MyMoneyXmlContentHandler2::writeBudget(bud, d, e); // write the XML to document
1492     query.bindValue(":id", bud.id());
1493     query.bindValue(":name", bud.name());
1494     query.bindValue(":start", bud.budgetStart());
1495     query.bindValue(":XML", d.toString());
1496     if (!query.exec()) // krazy:exclude=crashy
1497       throw MYMONEYEXCEPTIONSQL("writing Budgets"); // krazy:exclude=crashy
1498   }
1499 
writeKeyValuePairs(const QString & kvpType,const QVariantList & kvpId,const QList<QMap<QString,QString>> & pairs)1500   void writeKeyValuePairs(const QString& kvpType, const QVariantList& kvpId, const QList<QMap<QString, QString> >& pairs)
1501   {
1502     Q_Q(MyMoneyStorageSql);
1503     if (pairs.empty())
1504       return;
1505 
1506     QVariantList type;
1507     QVariantList id;
1508     QVariantList key;
1509     QVariantList value;
1510     int pairCount = 0;
1511 
1512     for (int i = 0; i < kvpId.size(); ++i) {
1513       QMap<QString, QString>::ConstIterator it;
1514       for (it = pairs[i].constBegin(); it != pairs[i].constEnd(); ++it) {
1515         type << kvpType;
1516         id << kvpId[i];
1517         key << it.key();
1518         value << it.value();
1519       }
1520       pairCount += pairs[i].size();
1521     }
1522 
1523     QSqlQuery query(*q);
1524     query.prepare(m_db.m_tables["kmmKeyValuePairs"].insertString());
1525     query.bindValue(":kvpType", type);
1526     query.bindValue(":kvpId", id);
1527     query.bindValue(":kvpKey", key);
1528     query.bindValue(":kvpData", value);
1529     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing KVP");
1530     m_kvps += pairCount;
1531   }
1532 
writeOnlineJob(const onlineJob & job,QSqlQuery & query)1533   void writeOnlineJob(const onlineJob& job, QSqlQuery& query)
1534   {
1535     Q_ASSERT(job.id().startsWith('O'));
1536 
1537     query.bindValue(":id", job.id());
1538     query.bindValue(":type", job.taskIid());
1539     query.bindValue(":jobSend", job.sendDate());
1540     query.bindValue(":bankAnswerDate", job.bankAnswerDate());
1541     switch (job.bankAnswerState()) {
1542       case eMyMoney::OnlineJob::sendingState::acceptedByBank: query.bindValue(":state", QLatin1String("acceptedByBank")); break;
1543       case eMyMoney::OnlineJob::sendingState::rejectedByBank: query.bindValue(":state", QLatin1String("rejectedByBank")); break;
1544       case eMyMoney::OnlineJob::sendingState::abortedByUser: query.bindValue(":state", QLatin1String("abortedByUser")); break;
1545       case eMyMoney::OnlineJob::sendingState::sendingError: query.bindValue(":state", QLatin1String("sendingError")); break;
1546       case eMyMoney::OnlineJob::sendingState::noBankAnswer:
1547       default: query.bindValue(":state", QLatin1String("noBankAnswer"));
1548     }
1549     query.bindValue(":locked", QVariant::fromValue<QString>(job.isLocked() ? QLatin1String("Y") : QLatin1String("N")));
1550   }
1551 
writePayeeIdentifier(const payeeIdentifier & pid,QSqlQuery & query)1552   void writePayeeIdentifier(const payeeIdentifier& pid, QSqlQuery& query)
1553   {
1554     query.bindValue(":id", pid.idString());
1555     query.bindValue(":type", pid.iid());
1556     if (!query.exec()) { // krazy:exclude=crashy
1557       qWarning() << buildError(query, Q_FUNC_INFO, QString("modifying payeeIdentifier"));
1558       throw MYMONEYEXCEPTIONSQL("modifying payeeIdentifier"); // krazy:exclude=crashy
1559     }
1560   }
1561     /** @} */
1562 
1563   /**
1564    * @name readMethods
1565    * @{
1566    */
readFileInfo()1567   void readFileInfo()
1568   {
1569     Q_Q(MyMoneyStorageSql);
1570     signalProgress(0, 1, QObject::tr("Loading file information..."));
1571 
1572     QSqlQuery query(*q);
1573 
1574     query.prepare(
1575       "SELECT "
1576       "  created, lastModified, "
1577       "  encryptData, logonUser, logonAt, "
1578       "  (SELECT count(*) FROM kmmInstitutions) AS institutions, "
1579       "  (SELECT count(*) from kmmAccounts) AS accounts, "
1580       "  (SELECT count(*) FROM kmmCurrencies) AS currencies, "
1581       "  (SELECT count(*) FROM kmmPayees) AS payees, "
1582       "  (SELECT count(*) FROM kmmTags) AS tags, "
1583       "  (SELECT count(*) FROM kmmTransactions) AS transactions, "
1584       "  (SELECT count(*) FROM kmmSplits) AS splits, "
1585       "  (SELECT count(*) FROM kmmSecurities) AS securities, "
1586       "  (SELECT count(*) FROM kmmCurrencies) AS currencies, "
1587       "  (SELECT count(*) FROM kmmSchedules) AS schedules, "
1588       "  (SELECT count(*) FROM kmmPrices) AS prices, "
1589       "  (SELECT count(*) FROM kmmKeyValuePairs) AS kvps, "
1590       "  (SELECT count(*) FROM kmmReportConfig) AS reports, "
1591       "  (SELECT count(*) FROM kmmBudgetConfig) AS budgets, "
1592       "  (SELECT count(*) FROM kmmOnlineJobs) AS onlineJobs, "
1593       "  (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier "
1594       "FROM kmmFileInfo;"
1595     );
1596 
1597     if (!query.exec())
1598       throw MYMONEYEXCEPTIONSQL("reading FileInfo"); // krazy:exclude=crashy
1599     if (!query.next())
1600       throw MYMONEYEXCEPTIONSQL("retrieving FileInfo");
1601 
1602     QSqlRecord rec = query.record();
1603     m_storage->setCreationDate(GETDATE(rec.indexOf("created")));
1604     m_storage->setLastModificationDate(GETDATE(rec.indexOf("lastModified")));
1605 
1606     m_institutions = (ulong) GETULL(rec.indexOf("institutions"));
1607     m_accounts = (ulong) GETULL(rec.indexOf("accounts"));
1608     m_payees = (ulong) GETULL(rec.indexOf("payees"));
1609     m_tags = (ulong) GETULL(rec.indexOf("tags"));
1610     m_transactions = (ulong) GETULL(rec.indexOf("transactions"));
1611     m_splits = (ulong) GETULL(rec.indexOf("splits"));
1612     m_securities = (ulong) GETULL(rec.indexOf("securities"));
1613     m_currencies = (ulong) GETULL(rec.indexOf("currencies"));
1614     m_schedules = (ulong) GETULL(rec.indexOf("schedules"));
1615     m_prices = (ulong) GETULL(rec.indexOf("prices"));
1616     m_kvps = (ulong) GETULL(rec.indexOf("kvps"));
1617     m_reports = (ulong) GETULL(rec.indexOf("reports"));
1618     m_budgets = (ulong) GETULL(rec.indexOf("budgets"));
1619     m_onlineJobs = (ulong) GETULL(rec.indexOf("onlineJobs"));
1620     m_payeeIdentifier = (ulong) GETULL(rec.indexOf("payeeIdentifier"));
1621 
1622     m_encryptData = GETSTRING(rec.indexOf("encryptData"));
1623     m_logonUser = GETSTRING(rec.indexOf("logonUser"));
1624     m_logonAt = GETDATETIME(rec.indexOf("logonAt"));
1625 
1626     signalProgress(1, 0);
1627     m_storage->setPairs(readKeyValuePairs("STORAGE", QString("")).pairs());
1628   }
1629 
1630   void readLogonData();
1631   void readUserInformation();
1632 
readInstitutions()1633   void readInstitutions()
1634   {
1635     Q_Q(MyMoneyStorageSql);
1636     try {
1637       QMap<QString, MyMoneyInstitution> iList = q->fetchInstitutions();
1638       m_storage->loadInstitutions(iList);
1639       readFileInfo();
1640     } catch (const MyMoneyException &) {
1641       throw;
1642     }
1643   }
1644 
readAccounts()1645   void readAccounts()
1646   {
1647     Q_Q(MyMoneyStorageSql);
1648     m_storage->loadAccounts(q->fetchAccounts());
1649   }
1650 
readTransactions(const QString & tidList,const QString & dateClause)1651   void readTransactions(const QString& tidList, const QString& dateClause)
1652   {
1653     Q_Q(MyMoneyStorageSql);
1654     try {
1655       m_storage->loadTransactions(q->fetchTransactions(tidList, dateClause));
1656     } catch (const MyMoneyException &) {
1657       throw;
1658     }
1659   }
1660 
readTransactions()1661   void readTransactions()
1662   {
1663     readTransactions(QString(), QString());
1664   }
1665 
readSplit(const QSqlQuery & query)1666   MyMoneySplit readSplit(const QSqlQuery& query) const
1667   {
1668     Q_Q(const MyMoneyStorageSql);
1669     // Set these up as statics, since the field numbers should not change
1670     // during execution.
1671     static const MyMoneyDbTable& t = m_db.m_tables["kmmSplits"];
1672     static const int splitIdCol = t.fieldNumber("splitId");
1673     static const int transactionIdCol = t.fieldNumber("transactionId");
1674     static const int payeeIdCol = t.fieldNumber("payeeId");
1675     static const int reconcileDateCol = t.fieldNumber("reconcileDate");
1676     static const int actionCol = t.fieldNumber("action");
1677     static const int reconcileFlagCol = t.fieldNumber("reconcileFlag");
1678     static const int valueCol = t.fieldNumber("value");
1679     static const int sharesCol = t.fieldNumber("shares");
1680     static const int priceCol = t.fieldNumber("price");
1681     static const int memoCol = t.fieldNumber("memo");
1682     static const int accountIdCol = t.fieldNumber("accountId");
1683     static const int costCenterIdCol = t.fieldNumber("costCenterId");
1684     static const int checkNumberCol = t.fieldNumber("checkNumber");
1685   //  static const int postDateCol = t.fieldNumber("postDate"); // FIXME - when Tom puts date into split object
1686     static const int bankIdCol = t.fieldNumber("bankId");
1687 
1688     MyMoneySplit s;
1689 
1690     QList<QString> tagIdList;
1691     QSqlQuery query1(*const_cast <MyMoneyStorageSql*>(q));
1692     query1.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId");
1693     query1.bindValue(":id", GETSTRING(splitIdCol));
1694     query1.bindValue(":transactionId", GETSTRING(transactionIdCol));
1695     if (!query1.exec()) throw MYMONEYEXCEPTIONSQL("reading tagId in Split"); // krazy:exclude=crashy
1696     while (query1.next())
1697       tagIdList << query1.value(0).toString();
1698 
1699     s.setTagIdList(tagIdList);
1700     s.setPayeeId(GETSTRING(payeeIdCol));
1701     s.setReconcileDate(GETDATE(reconcileDateCol));
1702     s.setAction(GETSTRING(actionCol));
1703     s.setReconcileFlag(static_cast<Split::State>(GETINT(reconcileFlagCol)));
1704     s.setValue(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(valueCol))));
1705     s.setShares(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(sharesCol))));
1706     s.setPrice(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(priceCol))));
1707     s.setMemo(GETSTRING(memoCol));
1708     s.setAccountId(GETSTRING(accountIdCol));
1709     s.setCostCenterId(GETSTRING(costCenterIdCol));
1710     s.setNumber(GETSTRING(checkNumberCol));
1711     //s.setPostDate(GETDATETIME(postDateCol)); // FIXME - when Tom puts date into split object
1712     s.setBankID(GETSTRING(bankIdCol));
1713 
1714     return s;
1715   }
1716 
readKeyValuePairs(const QString & kvpType,const QString & kvpId)1717   const MyMoneyKeyValueContainer readKeyValuePairs(const QString& kvpType, const QString& kvpId) const
1718   {
1719     Q_Q(const MyMoneyStorageSql);
1720     MyMoneyKeyValueContainer list;
1721     QSqlQuery query(*const_cast <MyMoneyStorageSql*>(q));
1722     query.prepare("SELECT kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type and kvpId = :id;");
1723     query.bindValue(":type", kvpType);
1724     query.bindValue(":id", kvpId);
1725     if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp for %1 %2").arg(kvpType) // krazy:exclude=crashy
1726                                             .arg(kvpId));
1727     while (query.next()) list.setValue(query.value(0).toString(), query.value(1).toString());
1728     return (list);
1729   }
1730 
readKeyValuePairs(const QString & kvpType,const QStringList & kvpIdList)1731   const QHash<QString, MyMoneyKeyValueContainer> readKeyValuePairs(const QString& kvpType, const QStringList& kvpIdList) const
1732   {
1733     Q_Q(const MyMoneyStorageSql);
1734     QHash<QString, MyMoneyKeyValueContainer> retval;
1735 
1736     QSqlQuery query(*const_cast <MyMoneyStorageSql*>(q));
1737 
1738     QString idList;
1739     if (!kvpIdList.empty()) {
1740       idList = QString(" and kvpId IN ('%1')").arg(kvpIdList.join("', '"));
1741     }
1742 
1743     QString sQuery = QString("SELECT kvpId, kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type %1 order by kvpId;").arg(idList);
1744 
1745     query.prepare(sQuery);
1746     query.bindValue(":type", kvpType);
1747     if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp List for %1").arg(kvpType)); // krazy:exclude=crashy
1748 
1749     // Reserve enough space for all values.
1750     retval.reserve(kvpIdList.size());
1751 
1752     // The loop below is designed to limit the number of calls to
1753     // QHash::operator[] in order to speed up calls to this function. This
1754     // assumes that QString::operator== is faster.
1755     /*
1756       if (q.next()) {
1757         QString oldkey = q.value(0).toString();
1758         MyMoneyKeyValueContainer& kvpc = retval[oldkey];
1759 
1760         kvpc.setValue(q.value(1).toString(), q.value(2).toString());
1761 
1762         while (q.next()) {
1763           if (q.value(0).toString() != oldkey) {
1764             oldkey = q.value(0).toString();
1765             kvpc = retval[oldkey];
1766           }
1767           kvpc.setValue(q.value(1).toString(), q.value(2).toString());
1768         }
1769       }
1770     */
1771     const bool isOnlineBanking = kvpType.toLower().compare(QLatin1String("onlinebanking")) == 0;
1772     while (query.next()) {
1773       QString kvpId = query.value(0).toString();
1774       QString kvpKey = query.value(1).toString();
1775       QString kvpData = query.value(2).toString();
1776       if (isOnlineBanking) {
1777         if ((kvpKey.toLower().compare(QLatin1String("provider")) == 0)
1778         && (kvpData.toLower().compare(QLatin1String("kmymoney ofx")) == 0)) {
1779           kvpData = QStringLiteral("ofximporter");
1780         }
1781       }
1782       retval[kvpId].setValue(kvpKey, kvpData);
1783     }
1784     return (retval);
1785   }
1786 
readSchedules()1787   void readSchedules()
1788   {
1789     Q_Q(MyMoneyStorageSql);
1790     try {
1791       m_storage->loadSchedules(q->fetchSchedules());
1792     } catch (const MyMoneyException &) {
1793       throw;
1794     }
1795 
1796   }
1797 
readSecurities()1798   void readSecurities()
1799   {
1800     Q_Q(MyMoneyStorageSql);
1801     try {
1802       m_storage->loadSecurities(q->fetchSecurities());
1803     } catch (const MyMoneyException &) {
1804       throw;
1805     }
1806 
1807   }
1808 
readPrices()1809   void readPrices()
1810   {
1811     Q_Q(MyMoneyStorageSql);
1812     try {
1813       m_storage->loadPrices(q->fetchPrices());
1814     } catch (const MyMoneyException &) {
1815       throw;
1816     }
1817   }
1818 
readCurrencies()1819   void readCurrencies()
1820   {
1821     Q_Q(MyMoneyStorageSql);
1822     try {
1823       m_storage->loadCurrencies(q->fetchCurrencies());
1824     } catch (const MyMoneyException &) {
1825       throw;
1826     }
1827   }
1828 
readReports()1829   void readReports()
1830   {
1831     Q_Q(MyMoneyStorageSql);
1832     try {
1833       m_storage->loadReports(q->fetchReports());
1834     } catch (const MyMoneyException &) {
1835       throw;
1836     }
1837   }
1838 
readBudgets()1839   void readBudgets()
1840   {
1841     Q_Q(MyMoneyStorageSql);
1842     m_storage->loadBudgets(q->fetchBudgets());
1843   }
1844 
readOnlineJobs()1845   void readOnlineJobs()
1846   {
1847     Q_Q(MyMoneyStorageSql);
1848     m_storage->loadOnlineJobs(q->fetchOnlineJobs());
1849   }
1850 
1851   /** @} */
1852 
deleteTransaction(const QString & id)1853   void deleteTransaction(const QString& id)
1854   {
1855     Q_Q(MyMoneyStorageSql);
1856     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
1857     QSqlQuery query(*q);
1858     QVariantList idList;
1859     idList << id;
1860     query.prepare("DELETE FROM kmmSplits WHERE transactionId = :transactionId;");
1861     query.bindValue(":transactionId", idList);
1862     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits");
1863 
1864     query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' "
1865               "AND kvpId LIKE '?%'");
1866     query.bindValue(1, idList);
1867     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits KVP");
1868 
1869     m_splits -= query.numRowsAffected();
1870     deleteKeyValuePairs("TRANSACTION", idList);
1871     query.prepare(m_db.m_tables["kmmTransactions"].deleteString());
1872     query.bindValue(":id", idList);
1873     if (!query.execBatch())
1874       throw MYMONEYEXCEPTIONSQL("deleting Transaction");
1875   }
1876 
deleteTagSplitsList(const QString & txId,const QList<int> & splitIdList)1877   void deleteTagSplitsList(const QString& txId, const QList<int>& splitIdList)
1878   {
1879     Q_Q(MyMoneyStorageSql);
1880     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
1881     QVariantList iList;
1882     QVariantList transactionIdList;
1883 
1884     // qCopy segfaults here, so do it with a hand-rolled loop
1885     foreach (int it_s, splitIdList) {
1886       iList << it_s;
1887       transactionIdList << txId;
1888     }
1889     QSqlQuery query(*q);
1890     query.prepare("DELETE FROM kmmTagSplits WHERE transactionId = :transactionId AND splitId = :splitId");
1891     query.bindValue(":splitId", iList);
1892     query.bindValue(":transactionId", transactionIdList);
1893     if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting tagSplits");
1894   }
1895 
deleteSchedule(const QString & id)1896   void deleteSchedule(const QString& id)
1897   {
1898     Q_Q(MyMoneyStorageSql);
1899     deleteTransaction(id);
1900     QSqlQuery query(*q);
1901     query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id");
1902     query.bindValue(":id", id);
1903     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy
1904     query.prepare(m_db.m_tables["kmmSchedules"].deleteString());
1905     query.bindValue(":id", id);
1906     if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule"); // krazy:exclude=crashy
1907     //FIXME: enable when schedules have KVPs.
1908     //deleteKeyValuePairs("SCHEDULE", id);
1909   }
1910 
deleteKeyValuePairs(const QString & kvpType,const QVariantList & idList)1911   void deleteKeyValuePairs(const QString& kvpType, const QVariantList& idList)
1912   {
1913     Q_Q(MyMoneyStorageSql);
1914     QSqlQuery query(*q);
1915     query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = :kvpType AND kvpId = :kvpId;");
1916     QVariantList typeList;
1917     for (int i = 0; i < idList.size(); ++i) {
1918       typeList << kvpType;
1919     }
1920     query.bindValue(":kvpType", typeList);
1921     query.bindValue(":kvpId", idList);
1922     if (!query.execBatch()) {
1923       QString idString;
1924       for (int i = 0; i < idList.size(); ++i) {
1925         idString.append(idList[i].toString() + ' ');
1926       }
1927       throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("deleting kvp for %1 %2").arg(kvpType).arg(idString));
1928     }
1929     m_kvps -= query.numRowsAffected();
1930   }
1931 
calcHighId(ulong i,const QString & id)1932   ulong calcHighId(ulong i, const QString& id)
1933   {
1934     QString nid = id;
1935     ulong high = (ulong) nid.remove(QRegExp("[A-Z]*")).toULongLong();
1936     return std::max(high, i);
1937   }
1938 
1939   void setVersion(const QString& version);
1940 
splitState(const TransactionFilter::State & state)1941   int splitState(const TransactionFilter::State& state) const
1942   {
1943     auto rc = (int)Split::State::NotReconciled;
1944 
1945     switch (state) {
1946       default:
1947       case TransactionFilter::State::NotReconciled:
1948         break;
1949 
1950       case TransactionFilter::State::Cleared:
1951         rc = (int)Split::State::Cleared;
1952         break;
1953 
1954       case TransactionFilter::State::Reconciled:
1955         rc = (int)Split::State::Reconciled;
1956         break;
1957 
1958       case TransactionFilter::State::Frozen:
1959         rc = (int)Split::State::Frozen;
1960         break;
1961     }
1962     return rc;
1963   }
1964 
getDate(const QString & date)1965   QDate getDate(const QString& date) const
1966   {
1967     return (date.isNull() ? QDate() : QDate::fromString(date, Qt::ISODate));
1968   }
1969 
getDateTime(const QString & date)1970   QDateTime getDateTime(const QString& date) const
1971   {
1972     return (date.isNull() ? QDateTime() : QDateTime::fromString(date, Qt::ISODate));
1973   }
1974 
fileExists(const QString & dbName)1975   bool fileExists(const QString& dbName)
1976   {
1977     QFile f(dbName);
1978     if (!f.exists()) {
1979       m_error = i18n("SQLite file %1 does not exist", dbName);
1980       return (false);
1981     }
1982     return (true);
1983   }
1984 
1985   /** @brief a function to build a comprehensive error message for an SQL error */
buildError(const QSqlQuery & query,const QString & function,const QString & messageb)1986   QString& buildError(const QSqlQuery& query, const QString& function,
1987                                          const QString& messageb) const
1988   {
1989     Q_Q(const MyMoneyStorageSql);
1990     return (buildError(query, function, messageb, q));
1991   }
1992 
buildError(const QSqlQuery & query,const QString & function,const QString & message,const QSqlDatabase * db)1993   QString& buildError(const QSqlQuery& query, const QString& function,
1994                                          const QString& message, const QSqlDatabase* db) const
1995   {
1996     Q_Q(const MyMoneyStorageSql);
1997     QString s = QString("Error in function %1 : %2").arg(function).arg(message);
1998     s += QString("\nDriver = %1, Host = %2, User = %3, Database = %4")
1999          .arg(db->driverName()).arg(db->hostName()).arg(db->userName()).arg(db->databaseName());
2000     QSqlError e = db->lastError();
2001     s += QString("\nDriver Error: %1").arg(e.driverText());
2002     s += QString("\nDatabase Error No %1: %2").arg(e.number()).arg(e.databaseText());
2003     s += QString("\nText: %1").arg(e.text());
2004     s += QString("\nError type %1").arg(e.type());
2005     e = query.lastError();
2006     s += QString("\nExecuted: %1").arg(query.executedQuery());
2007     s += QString("\nQuery error No %1: %2").arg(e.number()).arg(e.text());
2008     s += QString("\nError type %1").arg(e.type());
2009 
2010     const_cast <MyMoneyStorageSql*>(q)->d_func()->m_error = s;
2011     qDebug("%s", qPrintable(s));
2012     const_cast <MyMoneyStorageSql*>(q)->cancelCommitUnit(function);
2013     return (const_cast <MyMoneyStorageSql*>(q)->d_func()->m_error);
2014   }
2015 
2016   /**
2017    * MyMoneyStorageSql create database
2018    *
2019    * @param url pseudo-URL of database to be opened
2020    *
2021    * @return true - creation successful
2022    * @return false - could not create
2023    *
2024    */
2025 
createDatabase(const QUrl & url)2026   bool createDatabase(const QUrl &url)
2027   {
2028     Q_Q(MyMoneyStorageSql);
2029     int rc = true;
2030     if (!m_driver->requiresCreation()) return(true); // not needed for sqlite
2031     QString dbName = url.path().right(url.path().length() - 1); // remove separator slash
2032     if (!m_driver->canAutocreate()) {
2033       m_error = i18n("Automatic database creation for type %1 is not currently implemented.\n"
2034                      "Please create database %2 manually", q->driverName(), dbName);
2035       return (false);
2036     }
2037     // create the database (only works for mysql and postgre at present)
2038     { // for this code block, see QSqlDatabase API re removeDatabase
2039       QSqlDatabase maindb = QSqlDatabase::addDatabase(q->driverName(), "main");
2040       maindb.setDatabaseName(m_driver->defaultDbName());
2041       maindb.setHostName(url.host());
2042       maindb.setUserName(url.userName());
2043       maindb.setPassword(url.password());
2044       if (!maindb.open()) {
2045         throw MYMONEYEXCEPTION(QString::fromLatin1("opening database %1 in function %2")
2046                                   .arg(maindb.databaseName()).arg(Q_FUNC_INFO));
2047       } else {
2048         QSqlQuery qm(maindb);
2049         qm.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(q->password()));
2050         QString qs = m_driver->createDbString(dbName) + ';';
2051         if (!qm.exec(qs)) { // krazy:exclude=crashy
2052           buildError(qm, Q_FUNC_INFO,
2053                      i18n("Error in create database %1; do you have create permissions?", dbName), &maindb);
2054           rc = false;
2055         }
2056         maindb.close();
2057       }
2058     }
2059     QSqlDatabase::removeDatabase("main");
2060     return (rc);
2061   }
2062 
2063 
upgradeDb()2064   int upgradeDb()
2065   {
2066     Q_Q(MyMoneyStorageSql);
2067     //signalProgress(0, 1, QObject::tr("Upgrading database..."));
2068     QSqlQuery query(*q);
2069     query.prepare("SELECT version FROM kmmFileInfo;");
2070     if (!query.exec() || !query.next()) { // krazy:exclude=crashy
2071       if (!m_newDatabase) {
2072         buildError(query, Q_FUNC_INFO, "Error retrieving file info (version)");
2073         return(1);
2074       } else {
2075         m_dbVersion = m_db.currentVersion();
2076         m_storage->setFileFixVersion(m_storage->currentFixVersion());
2077         QSqlQuery query2(*q);
2078         query2.prepare("UPDATE kmmFileInfo SET version = :version, \
2079                   fixLevel = :fixLevel;");
2080         query2.bindValue(":version", m_dbVersion);
2081         query2.bindValue(":fixLevel", m_storage->currentFixVersion());
2082         if (!query2.exec()) { // krazy:exclude=crashy
2083           buildError(query2, Q_FUNC_INFO, "Error updating file info(version)");
2084           return(1);
2085         }
2086         return (0);
2087       }
2088     }
2089     // prior to dbv6, 'version' format was 'dbversion.fixLevel+1'
2090     // as of dbv6, these are separate fields
2091     QString version = query.value(0).toString();
2092     if (version.contains('.')) {
2093       m_dbVersion = query.value(0).toString().section('.', 0, 0).toUInt();
2094       m_storage->setFileFixVersion(query.value(0).toString().section('.', 1, 1).toUInt() - 1);
2095     } else {
2096       m_dbVersion = version.toUInt();
2097       query.prepare("SELECT fixLevel FROM kmmFileInfo;");
2098       if (!query.exec() || !query.next()) { // krazy:exclude=crashy
2099         buildError(query, Q_FUNC_INFO, "Error retrieving file info (fixLevel)");
2100         return(1);
2101       }
2102       m_storage->setFileFixVersion(query.value(0).toUInt());
2103     }
2104 
2105     if (m_dbVersion == m_db.currentVersion())
2106       return 0;
2107 
2108     int rc = 0;
2109 
2110     // Drop VIEWs
2111     QStringList lowerTables = tables(QSql::AllTables);
2112     for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) {
2113       (*i) = (*i).toLower();
2114     }
2115 
2116     for (QMap<QString, MyMoneyDbView>::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) {
2117       if (lowerTables.contains(tt.key().toLower())) {
2118         if (!query.exec("DROP VIEW " + tt.value().name() + ';')) // krazy:exclude=crashy
2119           throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("dropping view %1").arg(tt.key()));
2120       }
2121     }
2122 
2123     while ((m_dbVersion < m_db.currentVersion()) && (rc == 0)) {
2124       qDebug() << "Perform upgrade to db layout version" << m_dbVersion+1;
2125       query.finish();
2126       switch (m_dbVersion) {
2127         case 0:
2128           if ((rc = upgradeToV1()) != 0) return (1);
2129           ++m_dbVersion;
2130           break;
2131         case 1:
2132           if ((rc = upgradeToV2()) != 0) return (1);
2133           ++m_dbVersion;
2134           break;
2135         case 2:
2136           if ((rc = upgradeToV3()) != 0) return (1);
2137           ++m_dbVersion;
2138           break;
2139         case 3:
2140           if ((rc = upgradeToV4()) != 0) return (1);
2141           ++m_dbVersion;
2142           break;
2143         case 4:
2144           if ((rc = upgradeToV5()) != 0) return (1);
2145           ++m_dbVersion;
2146           break;
2147         case 5:
2148           if ((rc = upgradeToV6()) != 0) return (1);
2149           ++m_dbVersion;
2150           break;
2151         case 6:
2152           if ((rc = upgradeToV7()) != 0) return (1);
2153           ++m_dbVersion;
2154           break;
2155         case 7:
2156           if ((rc = upgradeToV8()) != 0) return (1);
2157           ++m_dbVersion;
2158           break;
2159         case 8:
2160           if ((rc = upgradeToV9()) != 0) return (1);
2161           ++m_dbVersion;
2162           break;
2163         case 9:
2164           if ((rc = upgradeToV10()) != 0) return (1);
2165           ++m_dbVersion;
2166           break;
2167         case 10:
2168           if ((rc = upgradeToV11()) != 0) return (1);
2169           ++m_dbVersion;
2170           break;
2171         case 11:
2172           if ((rc = upgradeToV12()) != 0) return (1);
2173           ++m_dbVersion;
2174           break;
2175         default:
2176           qWarning("Unknown version number in database - %d", m_dbVersion);
2177       }
2178     }
2179     query.finish();
2180 
2181     // restore VIEWs
2182     lowerTables = tables(QSql::AllTables);
2183     for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) {
2184       (*i) = (*i).toLower();
2185     }
2186 
2187     for (QMap<QString, MyMoneyDbView>::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) {
2188       if (!lowerTables.contains(tt.key().toLower())) {
2189         if (!query.exec(tt.value().createString())) // krazy:exclude=crashy
2190           throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key()));
2191       }
2192     }
2193 
2194     // write updated version to DB
2195     //setVersion(QString("%1.%2").arg(m_dbVersion).arg(m_minorVersion))
2196     query.prepare(QString("UPDATE kmmFileInfo SET version = :version;"));
2197     query.bindValue(":version", m_dbVersion);
2198     if (!query.exec()) { // krazy:exclude=crashy
2199       buildError(query, Q_FUNC_INFO, "Error updating db version");
2200       return (1);
2201     }
2202     //signalProgress(-1,-1);
2203     return (0);
2204   }
2205 
upgradeToV1()2206   int upgradeToV1()
2207   {
2208     Q_Q(MyMoneyStorageSql);
2209     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
2210     QSqlQuery query(*q);
2211     // change kmmSplits pkey to (transactionId, splitId)
2212     if (!query.exec("ALTER TABLE kmmSplits ADD PRIMARY KEY (transactionId, splitId);")) { // krazy:exclude=crashy
2213       buildError(query, Q_FUNC_INFO, "Error updating kmmSplits pkey");
2214       return (1);
2215     }
2216     // change kmmSplits alter checkNumber varchar(32)
2217     if (!query.exec(m_db.m_tables["kmmSplits"].modifyColumnString(m_driver, "checkNumber", // krazy:exclude=crashy
2218                 MyMoneyDbColumn("checkNumber", "varchar(32)")))) {
2219       buildError(query, Q_FUNC_INFO, "Error expanding kmmSplits.checkNumber");
2220       return (1);
2221     }
2222     // change kmmSplits add postDate datetime
2223     if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion))
2224       return (1);
2225     // initialize it to same value as transaction (do it the long way round)
2226     query.prepare("SELECT id, postDate FROM kmmTransactions WHERE txType = 'N';");
2227     if (!query.exec()) { // krazy:exclude=crashy
2228       buildError(query, Q_FUNC_INFO, "Error priming kmmSplits.postDate");
2229       return (1);
2230     }
2231     QMap<QString, QDateTime> tids;
2232     while (query.next()) tids[query.value(0).toString()] = query.value(1).toDateTime();
2233     QMap<QString, QDateTime>::ConstIterator it;
2234     for (it = tids.constBegin(); it != tids.constEnd(); ++it) {
2235       query.prepare("UPDATE kmmSplits SET postDate=:postDate WHERE transactionId = :id;");
2236       query.bindValue(":postDate", it.value().toString(Qt::ISODate));
2237       query.bindValue(":id", it.key());
2238       if (!query.exec()) { // krazy:exclude=crashy
2239         buildError(query, Q_FUNC_INFO, "priming kmmSplits.postDate");
2240         return(1);
2241       }
2242     }
2243     // add index to kmmKeyValuePairs to (kvpType,kvpId)
2244     QStringList list;
2245     list << "kvpType" << "kvpId";
2246     if (!query.exec(MyMoneyDbIndex("kmmKeyValuePairs", "kmmKVPtype_id", list, false).generateDDL(m_driver) + ';')) {
2247       buildError(query, Q_FUNC_INFO, "Error adding kmmKeyValuePairs index");
2248       return (1);
2249     }
2250     // add index to kmmSplits to (accountId, txType)
2251     list.clear();
2252     list << "accountId" << "txType";
2253     if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmSplitsaccount_type", list, false).generateDDL(m_driver) + ';')) {
2254       buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index");
2255       return (1);
2256     }
2257     // change kmmSchedulePaymentHistory pkey to (schedId, payDate)
2258     if (!query.exec("ALTER TABLE kmmSchedulePaymentHistory ADD PRIMARY KEY (schedId, payDate);")) {
2259       buildError(query, Q_FUNC_INFO, "Error updating kmmSchedulePaymentHistory pkey");
2260       return (1);
2261     }
2262     // change kmmPrices pkey to (fromId, toId, priceDate)
2263     if (!query.exec("ALTER TABLE kmmPrices ADD PRIMARY KEY (fromId, toId, priceDate);")) {
2264       buildError(query, Q_FUNC_INFO, "Error updating kmmPrices pkey");
2265       return (1);
2266     }
2267     // change kmmReportConfig pkey to (name)
2268     // There wasn't one previously, so no need to drop it.
2269     if (!query.exec("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (name);")) {
2270       buildError(query, Q_FUNC_INFO, "Error updating kmmReportConfig pkey");
2271       return (1);
2272     }
2273     // change kmmFileInfo add budgets, hiBudgetId unsigned bigint
2274     // change kmmFileInfo add logonUser
2275     // change kmmFileInfo add logonAt datetime
2276     if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion))
2277       return (1);
2278     // change kmmAccounts add transactionCount unsigned bigint as last field
2279     if (!alterTable(m_db.m_tables["kmmAccounts"], m_dbVersion))
2280       return (1);
2281     // calculate the transaction counts. the application logic defines an account's tx count
2282     // in such a way as to count multiple splits in a tx which reference the same account as one.
2283     // this is the only way I can think of to do this which will work in sqlite too.
2284     // inefficient, but it only gets done once...
2285     // get a list of all accounts so we'll get a zero value for those without txs
2286     query.prepare("SELECT id FROM kmmAccounts");
2287     if (!query.exec()) { // krazy:exclude=crashy
2288       buildError(query, Q_FUNC_INFO, "Error retrieving accounts for transaction counting");
2289       return(1);
2290     }
2291     while (query.next()) {
2292       m_transactionCountMap[query.value(0).toString()] = 0;
2293     }
2294     query.prepare("SELECT accountId, transactionId FROM kmmSplits WHERE txType = 'N' ORDER BY 1, 2");
2295     if (!query.exec()) { // krazy:exclude=crashy
2296       buildError(query, Q_FUNC_INFO, "Error retrieving splits for transaction counting");
2297       return(1);
2298     }
2299     QString lastAcc, lastTx;
2300     while (query.next()) {
2301       QString thisAcc = query.value(0).toString();
2302       QString thisTx = query.value(1).toString();
2303       if ((thisAcc != lastAcc) || (thisTx != lastTx)) ++m_transactionCountMap[thisAcc];
2304       lastAcc = thisAcc;
2305       lastTx = thisTx;
2306     }
2307     QHash<QString, ulong>::ConstIterator itm;
2308     query.prepare("UPDATE kmmAccounts SET transactionCount = :txCount WHERE id = :id;");
2309     for (itm = m_transactionCountMap.constBegin(); itm != m_transactionCountMap.constEnd(); ++itm) {
2310       query.bindValue(":txCount", QString::number(itm.value()));
2311       query.bindValue(":id", itm.key());
2312       if (!query.exec()) { // krazy:exclude=crashy
2313         buildError(query, Q_FUNC_INFO, "Error updating transaction count");
2314         return (1);
2315       }
2316     }
2317     m_transactionCountMap.clear();
2318     return (0);
2319   }
2320 
upgradeToV2()2321   int upgradeToV2()
2322   {
2323     Q_Q(MyMoneyStorageSql);
2324     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
2325     // change kmmSplits add price, priceFormatted fields
2326     if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion))
2327       return (1);
2328     return (0);
2329   }
2330 
upgradeToV3()2331   int upgradeToV3()
2332   {
2333     Q_Q(MyMoneyStorageSql);
2334     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
2335     QSqlQuery query(*q);
2336     // kmmSchedules - add occurrenceMultiplier
2337     // The default value is given here to populate the column.
2338     if (!query.exec("ALTER TABLE kmmSchedules ADD COLUMN " +
2339                 MyMoneyDbIntColumn("occurenceMultiplier",
2340                                    MyMoneyDbIntColumn::SMALL, false, false, true)
2341                 .generateDDL(m_driver) + " DEFAULT 0;")) {
2342       buildError(query, Q_FUNC_INFO, "Error adding kmmSchedules.occurenceMultiplier");
2343       return (1);
2344     }
2345     //The default is less than any useful value, so as each schedule is hit, it will update
2346     //itself to the appropriate value.
2347     return 0;
2348   }
2349 
upgradeToV4()2350   int upgradeToV4()
2351   {
2352     Q_Q(MyMoneyStorageSql);
2353     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
2354     QSqlQuery query(*q);
2355     // kmmSplits - add index on transactionId + splitId
2356     QStringList list;
2357     list << "transactionId" << "splitId";
2358     if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmTx_Split", list, false).generateDDL(m_driver) + ';')) {
2359       buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index on (transactionId, splitId)");
2360       return (1);
2361     }
2362     return 0;
2363   }
2364 
upgradeToV5()2365   int upgradeToV5()
2366   {
2367     Q_Q(MyMoneyStorageSql);
2368     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2369     // kmmSplits - add bankId
2370     if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion))
2371       return (1);
2372     //kmmPayees - add columns "notes" "defaultAccountId" "matchData" "matchIgnoreCase" "matchKeys";
2373     if (!alterTable(m_db.m_tables["kmmPayees"], m_dbVersion))
2374       return (1);
2375     // kmmReportConfig - drop primary key on name since duplicate names are allowed
2376     if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion))
2377       return (1);
2378     //}
2379     return 0;
2380   }
2381 
upgradeToV6()2382   int upgradeToV6()
2383   {
2384     Q_Q(MyMoneyStorageSql);
2385     q->startCommitUnit(Q_FUNC_INFO);
2386     QSqlQuery query(*q);
2387     // kmmFileInfo - add fixLevel
2388     if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion))
2389       return (1);
2390     // upgrade Mysql to InnoDB transaction-safe engine
2391     // the following is not a good way to test for mysql - think of a better way
2392     if (!m_driver->tableOptionString().isEmpty()) {
2393       for (QMap<QString, MyMoneyDbTable>::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) {
2394         if (!query.exec(QString("ALTER TABLE %1 ENGINE = InnoDB;").arg(tt.value().name()))) {
2395           buildError(query, Q_FUNC_INFO, "Error updating to InnoDB");
2396           return (1);
2397         }
2398       }
2399     }
2400 
2401     // the alterTable function really doesn't work too well
2402     // with adding a new column which is also to be primary key
2403     // so add the column first
2404     if (!query.exec("ALTER TABLE kmmReportConfig ADD COLUMN " +
2405                 MyMoneyDbColumn("id", "varchar(32)").generateDDL(m_driver) + ';')) {
2406       buildError(query, Q_FUNC_INFO, "adding id to report table");
2407       return(1);
2408     }
2409     QMap<QString, MyMoneyReport> reportList = q->fetchReports();
2410     // the V5 database allowed lots of duplicate reports with no
2411     // way to distinguish between them. The fetchReports call
2412     // will have effectively removed all duplicates
2413     // so we now delete from the db and re-write them
2414     if (!query.exec("DELETE FROM kmmReportConfig;")) {
2415       buildError(query, Q_FUNC_INFO, "Error deleting reports");
2416       return (1);
2417     }
2418     // add unique id to reports table
2419     if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion))
2420       return(1);
2421 
2422     QMap<QString, MyMoneyReport>::const_iterator it_r;
2423     for (it_r = reportList.constBegin(); it_r != reportList.constEnd(); ++it_r) {
2424       MyMoneyReport r = *it_r;
2425       query.prepare(m_db.m_tables["kmmReportConfig"].insertString());
2426       writeReport(*it_r, query);
2427     }
2428 
2429     q->endCommitUnit(Q_FUNC_INFO);
2430     return 0;
2431   }
2432 
upgradeToV7()2433   int upgradeToV7()
2434   {
2435     Q_Q(MyMoneyStorageSql);
2436     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2437 
2438     // add tags support
2439     // kmmFileInfo - add tags and hiTagId
2440     if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion))
2441       return (1);
2442 
2443     m_tags = 0;
2444     return 0;
2445   }
2446 
upgradeToV8()2447   int upgradeToV8()
2448   {
2449     Q_Q(MyMoneyStorageSql);
2450     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2451 
2452     // Added onlineJobs and payeeIdentifier
2453     if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion))
2454       return (1);
2455 
2456     return 0;
2457   }
2458 
upgradeToV9()2459   int upgradeToV9()
2460   {
2461     Q_Q(MyMoneyStorageSql);
2462     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2463 
2464     // kmmSplits - add bankId
2465     if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion))
2466       return (1);
2467 
2468     return 0;
2469   }
2470 
upgradeToV10()2471   int upgradeToV10()
2472   {
2473     Q_Q(MyMoneyStorageSql);
2474     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2475 
2476     if (!alterTable(m_db.m_tables["kmmPayeesPayeeIdentifier"], m_dbVersion))
2477       return (1);
2478     if (!alterTable(m_db.m_tables["kmmAccountsPayeeIdentifier"], m_dbVersion))
2479       return (1);
2480 
2481     return 0;
2482   }
2483 
upgradeToV11()2484   int upgradeToV11()
2485   {
2486     Q_Q(MyMoneyStorageSql);
2487     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2488 
2489     // add column roundingMethodCol to kmmSecurities
2490     if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion))
2491       return 1;
2492     // add column pricePrecision to kmmCurrencies
2493     if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion))
2494       return 1;
2495 
2496     return 0;
2497   }
2498 
upgradeToV12()2499   int upgradeToV12()
2500   {
2501     Q_Q(MyMoneyStorageSql);
2502     MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO);
2503 
2504     switch(haveColumnInTable(QLatin1String("kmmSchedules"), QLatin1String("lastDayInMonth"))) {
2505       case -1:
2506         return 1;
2507       case 1:         // column exists, nothing to do
2508         break;
2509       case 0:         // need update of kmmSchedules
2510         // add column lastDayInMonth. Simply redo the update for 10 .. 11
2511         if (!alterTable(m_db.m_tables["kmmSchedules"], m_dbVersion-1))
2512           return 1;
2513         break;
2514     }
2515 
2516     switch(haveColumnInTable(QLatin1String("kmmSecurities"), QLatin1String("roundingMethod"))) {
2517       case -1:
2518         return 1;
2519       case 1:         // column exists, nothing to do
2520         break;
2521       case 0:         // need update of kmmSecurities and kmmCurrencies
2522         // add column roundingMethodCol to kmmSecurities. Simply redo the update for 10 .. 11
2523         if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion-1))
2524           return 1;
2525         // add column pricePrecision to kmmCurrencies. Simply redo the update for 10 .. 11
2526         if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion-1))
2527           return 1;
2528         break;
2529     }
2530     return 0;
2531   }
2532 
createTables()2533   int createTables()
2534   {
2535     Q_Q(MyMoneyStorageSql);
2536     // check tables, create if required
2537     // convert everything to lower case, since SQL standard is case insensitive
2538     // table and column names (when not delimited), but some DBMSs disagree.
2539     QStringList lowerTables = tables(QSql::AllTables);
2540     for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) {
2541       (*i) = (*i).toLower();
2542     }
2543 
2544     for (QMap<QString, MyMoneyDbTable>::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) {
2545       if (!lowerTables.contains(tt.key().toLower())) {
2546         createTable(tt.value());
2547       }
2548     }
2549 
2550     QSqlQuery query(*q);
2551     for (QMap<QString, MyMoneyDbView>::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) {
2552       if (!lowerTables.contains(tt.key().toLower())) {
2553         if (!query.exec(tt.value().createString())) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key()));
2554       }
2555     }
2556 
2557     // The columns to store version info changed with version 6. Prior versions are not supported here but an error is prevented and
2558     // an old behaviour is used: call upgradeDb().
2559     m_dbVersion = m_db.currentVersion();
2560     if (m_dbVersion >= 6) {
2561       // create the fileinfo stuff if it does not exist
2562       query.prepare("SELECT count(*) FROM kmmFileInfo;");
2563       if (!query.exec() || !query.next())
2564         throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy
2565 
2566       if (query.value(0).toInt() == 0) {
2567         query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version, fixLevel) VALUES(?,?);"));
2568         query.bindValue(0, m_dbVersion);
2569         query.bindValue(1, m_storage->fileFixVersion());
2570         if (!query.exec())
2571           throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("Saving database version"));
2572       }
2573     }
2574     query.finish();
2575 
2576     return upgradeDb();
2577   }
2578 
2579   void createTable(const MyMoneyDbTable& t, int version = std::numeric_limits<int>::max())
2580   {
2581     Q_Q(MyMoneyStorageSql);
2582   // create the tables
2583     QStringList ql = t.generateCreateSQL(m_driver, version).split('\n', QString::SkipEmptyParts);
2584     QSqlQuery query(*q);
foreach(const QString & i,ql)2585     foreach (const QString& i, ql) {
2586       if (!query.exec(i)) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating table/index %1").arg(t.name()));
2587     }
2588   }
2589 
alterTable(const MyMoneyDbTable & t,int fromVersion)2590   bool alterTable(const MyMoneyDbTable& t, int fromVersion)
2591   {
2592     Q_Q(MyMoneyStorageSql);
2593     const int toVersion = fromVersion + 1;
2594 
2595     QString tempTableName = t.name();
2596     tempTableName.replace("kmm", "kmmtmp");
2597     QSqlQuery query(*q);
2598     // drop primary key if it has one (and driver supports it)
2599     if (t.hasPrimaryKey(fromVersion)) {
2600       QString dropString = m_driver->dropPrimaryKeyString(t.name());
2601       if (!dropString.isEmpty()) {
2602         if (!query.exec(dropString)) {
2603           buildError(query, Q_FUNC_INFO, QString("Error dropping old primary key from %1").arg(t.name()));
2604           return false;
2605         }
2606       }
2607     }
2608     query.finish();
2609     for (MyMoneyDbTable::index_iterator i = t.indexBegin(); i != t.indexEnd(); ++i) {
2610       QString indexName = t.name() + '_' + i->name() + "_idx";
2611       if (!query.exec(m_driver->dropIndexString(t.name(), indexName))) {
2612         buildError(query, Q_FUNC_INFO, QString("Error dropping index from %1").arg(t.name()));
2613         return false;
2614       }
2615       query.finish();
2616     }
2617     if (!query.exec(QString("ALTER TABLE " + t.name() + " RENAME TO " + tempTableName + ';'))) {
2618       buildError(query, Q_FUNC_INFO, QString("Error renaming table %1").arg(t.name()));
2619       return false;
2620     }
2621     query.finish();
2622     createTable(t, toVersion);
2623     if (q->getRecCount(tempTableName) > 0) {
2624       query.finish();
2625       query.prepare(QString("INSERT INTO " + t.name() + " (" + t.columnList(fromVersion, true) +
2626                           ") SELECT " + t.columnList(fromVersion, false) + " FROM " + tempTableName + ';'));
2627       if (!query.exec()) { // krazy:exclude=crashy
2628           buildError(query, Q_FUNC_INFO, QString("Error inserting into new table %1").arg(t.name()));
2629           return false;
2630       }
2631       query.finish();
2632     }
2633     if (!query.exec(QString("DROP TABLE " + tempTableName + ';'))) {
2634       buildError(query, Q_FUNC_INFO, QString("Error dropping old table %1").arg(t.name()));
2635       return false;
2636     }
2637     query.finish();
2638     return true;
2639   }
2640 
clean()2641   void clean()
2642   {
2643     Q_Q(MyMoneyStorageSql);
2644   // delete all existing records
2645     QMap<QString, MyMoneyDbTable>::ConstIterator it = m_db.tableBegin();
2646     QSqlQuery query(*q);
2647     while (it != m_db.tableEnd()) {
2648       query.prepare(QString("DELETE from %1;").arg(it.key()));
2649       if (!query.exec()) throw MYMONEYEXCEPTIONSQL("cleaning database"); // krazy:exclude=crashy
2650       ++it;
2651     }
2652   }
2653 
isEmpty()2654   int isEmpty()
2655   {
2656     Q_Q(MyMoneyStorageSql);
2657     // check all tables are empty
2658     QMap<QString, MyMoneyDbTable>::ConstIterator tt = m_db.tableBegin();
2659     int recordCount = 0;
2660     QSqlQuery query(*q);
2661     while ((tt != m_db.tableEnd()) && (recordCount == 0)) {
2662       query.prepare(QString("select count(*) from %1;").arg((*tt).name()));
2663       if (!query.exec()) throw MYMONEYEXCEPTIONSQL("getting record count"); // krazy:exclude=crashy
2664       if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving record count");
2665       recordCount += query.value(0).toInt();
2666       ++tt;
2667     }
2668 
2669     // a fresh created database contains at least one record (see createTables()) in
2670     // the kmmFileInfo table providing file and fix version. So we report empty
2671     // even if there is a recordCount of 1
2672     if (recordCount > 1) {
2673       return -1;    // not empty
2674     } else {
2675       return 0;
2676     }
2677   }
2678 
2679   // for bug 252841
tables(QSql::TableType tt)2680   QStringList tables(QSql::TableType tt)
2681   {
2682     Q_Q(MyMoneyStorageSql);
2683     return (m_driver->tables(tt, static_cast<const QSqlDatabase&>(*q)));
2684   }
2685 
2686   //! Returns 1 in case the @a column exists in @a table, 0 if not. In case of error, -1 is returned.
haveColumnInTable(const QString & table,const QString & column)2687   int haveColumnInTable(const QString& table, const QString& column)
2688   {
2689     Q_Q(MyMoneyStorageSql);
2690     QSqlQuery query(*q);
2691     QString cmd = QString("SELECT * FROM %1 LIMIT 1").arg(table);
2692     if(!query.exec(cmd)) {
2693       buildError(query, Q_FUNC_INFO, QString("Error detecting if %1 exists in %2").arg(column).arg(table));
2694       return -1;
2695     }
2696     QSqlRecord rec = query.record();
2697     return (rec.indexOf(column) != -1) ? 1 : 0;
2698   }
2699 
2700   /**
2701    * @brief Ensure the storagePlugin with iid was setup
2702    *
2703    * @throws MyMoneyException in case of an error which makes the use
2704    * of the plugin unavailable.
2705    */
setupStoragePlugin(QString iid)2706   bool setupStoragePlugin(QString iid)
2707   {
2708     Q_Q(MyMoneyStorageSql);
2709     // setupDatabase has to be called every time because this simple technique to check if was updated already
2710     // does not work if a user opens another file
2711     // also the setup is removed if the current database transaction is rolled back
2712     if (iid.isEmpty() /*|| m_loadedStoragePlugins.contains(iid)*/)
2713       return false;
2714 
2715     MyMoneyDbTransaction t(*q, Q_FUNC_INFO);
2716     auto rc = false;
2717     if (iid == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid())
2718       rc = setupIBANBIC(*q);
2719     else if (iid == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid())
2720       rc = setupNationalAccount(*q);
2721     else if (iid == sepaOnlineTransferImpl::name())
2722       rc = setupSepaOnlineTransfer(*q);
2723     else
2724       rc = false;
2725     return rc;
2726   }
2727 
setupIBANBIC(QSqlDatabase connection)2728   bool setupIBANBIC(QSqlDatabase connection)
2729   {
2730     auto iid = QLatin1String("org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin");
2731     // Get current version
2732     QSqlQuery query = QSqlQuery(connection);
2733     query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?");
2734     query.bindValue(0, iid);
2735     if (!query.exec()) {
2736       qWarning("Could not execute query for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text()));
2737       return false;
2738     }
2739 
2740     int currentVersion = 0;
2741     if (query.next())
2742       currentVersion = query.value(0).toInt();
2743 
2744     // Create database in it's most recent version if version is 0
2745     // (version 0 means the database was not installed)
2746     if (currentVersion == 0) {
2747       // If the database is recreated the table may be still there. So drop it if needed. No error handling needed
2748       // as this step is not necessary - only the creation is important.
2749       if (!query.exec("DROP TABLE IF EXISTS kmmIbanBic;"))
2750         return false;
2751 
2752       if (!query.exec(
2753             "CREATE TABLE kmmIbanBic ("
2754             "  id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE,"
2755             "  iban varchar(32),"
2756             "  bic char(11) CHECK(length(bic) = 11 OR bic IS NULL),"
2757             "  name text"
2758             " );"
2759           )) {
2760         qWarning("Could not create table for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text()));
2761         return false;
2762       }
2763 
2764       query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)");
2765       query.bindValue(0, iid);
2766       query.bindValue(1, 1);
2767       query.bindValue(2, 0);
2768       query.bindValue(3, "DROP TABLE kmmIbanBic;");
2769       if (query.exec())
2770         return true;
2771       qWarning("Could not save plugin info for ibanBicStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text()));
2772       return false;
2773     }
2774 
2775     // Check if version is valid with this plugin
2776     switch (currentVersion) {
2777       case 1: return true;
2778     }
2779 
2780     return false;
2781   }
2782 
setupNationalAccount(QSqlDatabase connection)2783   bool setupNationalAccount(QSqlDatabase connection)
2784   {
2785     auto iid = QLatin1String("org.kmymoney.payeeIdentifier.nationalAccount.sqlStoragePlugin");
2786     // Get current version
2787     QSqlQuery query = QSqlQuery(connection);
2788     query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?");
2789     query.bindValue(0, iid);
2790     if (!query.exec()) {
2791       qWarning("Could not execute query for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text()));
2792       return false;
2793     }
2794 
2795     int currentVersion = 0;
2796     if (query.next())
2797       currentVersion = query.value(0).toInt();
2798 
2799     // Create database in it's most recent version if version is 0
2800     // (version 0 means the database was not installed)
2801     if (currentVersion == 0) {
2802       // If the database is recreated the table may be still there. So drop it if needed. No error handling needed
2803       // as this step is not necessary - only the creation is important.
2804       if (!query.exec("DROP TABLE IF EXISTS kmmNationalAccountNumber;"))
2805         return false;
2806 
2807       if (!query.exec(
2808             "CREATE TABLE kmmNationalAccountNumber ("
2809             "  id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE,"
2810             "  countryCode varchar(3),"
2811             "  accountNumber TEXT,"
2812             "  bankCode TEXT,"
2813             "  name TEXT"
2814             " );"
2815           )) {
2816         qWarning("Could not create table for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text()));
2817         return false;
2818       }
2819 
2820       query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)");
2821       query.bindValue(0, iid);
2822       query.bindValue(1, 1);
2823       query.bindValue(2, 0);
2824       query.bindValue(3, "DROP TABLE kmmNationalAccountNumber;");
2825       if (query.exec())
2826         return true;
2827       qWarning("Could not save plugin info for nationalAccountStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text()));
2828       return false;
2829     }
2830 
2831     // Check if version is valid with this plugin
2832     switch (currentVersion) {
2833       case 1: return true;
2834     }
2835 
2836     return false;
2837   }
2838 
setupSepaOnlineTransfer(QSqlDatabase connection)2839   bool setupSepaOnlineTransfer(QSqlDatabase connection)
2840   {
2841     auto iid = QLatin1String("org.kmymoney.creditTransfer.sepa.sqlStoragePlugin");
2842     // Get current version
2843     QSqlQuery query = QSqlQuery(connection);
2844     query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?");
2845     query.bindValue(0, iid);
2846     if (!query.exec()) {
2847       qWarning("Could not execute query for sepaStoragePlugin: %s", qPrintable(query.lastError().text()));
2848       return false;
2849     }
2850 
2851     int currentVersion = 0;
2852     if (query.next())
2853       currentVersion = query.value(0).toInt();
2854 
2855     // Create database in it's most recent version if version is 0
2856     // (version 0 means the database was not installed)
2857     if (currentVersion <= 1) {
2858       // If the database is recreated the table may be still there. So drop it if needed. No error handling needed
2859       // as this step is not necessary - only the creation is important.
2860       if (!query.exec("DROP TABLE IF EXISTS kmmSepaOrders;"))
2861         return false;
2862 
2863       if (!query.exec(
2864             "CREATE TABLE kmmSepaOrders ("
2865             "  id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmOnlineJobs( id ) ON UPDATE CASCADE ON DELETE CASCADE,"
2866             "  originAccount varchar(32) REFERENCES kmmAccounts( id ) ON UPDATE CASCADE ON DELETE SET NULL,"
2867             "  value text,"
2868             "  purpose text,"
2869             "  endToEndReference varchar(35),"
2870             "  beneficiaryName varchar(27),"
2871             "  beneficiaryIban varchar(32),"
2872             "  beneficiaryBic char(11),"
2873             "  textKey int,"
2874             "  subTextKey int"
2875             " );"
2876           )) {
2877         qWarning("Error while creating table kmmSepaOrders: %s", qPrintable(query.lastError().text()));
2878         return false;
2879       }
2880 
2881       query.prepare("DELETE FROM kmmPluginInfo WHERE iid = ?;");
2882       query.bindValue(0, iid);
2883       query.exec();
2884 
2885       query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)");
2886       query.bindValue(0, iid);
2887       query.bindValue(1, 2);
2888       query.bindValue(2, 0);
2889       query.bindValue(3, "DROP TABLE kmmSepaOrders;");
2890       if (query.exec())
2891         return true;
2892       qWarning("Error while inserting kmmPluginInfo for '%s': %s", qPrintable(iid), qPrintable(query.lastError().text()));
2893       return false;
2894     }
2895 
2896     // Check if version is valid with this plugin
2897     switch (currentVersion) {
2898       case 2: return true;
2899     }
2900 
2901     return false;
2902   }
2903 
actOnIBANBICObjectInSQL(SQLAction action,const payeeIdentifier & obj)2904   bool actOnIBANBICObjectInSQL(SQLAction action, const payeeIdentifier &obj)
2905   {
2906     payeeIdentifierTyped<payeeIdentifiers::ibanBic> payeeIdentifier = payeeIdentifierTyped<payeeIdentifiers::ibanBic>(obj);
2907 
2908     Q_Q(MyMoneyStorageSql);
2909     QSqlQuery query(*q);
2910 
2911     auto writeQuery = [&]() {
2912       query.bindValue(":id", obj.idString());
2913       query.bindValue(":iban", payeeIdentifier->electronicIban());
2914       const auto bic = payeeIdentifier->fullStoredBic();
2915       query.bindValue(":bic", (bic.isEmpty()) ? QVariant(QVariant::String) : bic);
2916       query.bindValue(":name", payeeIdentifier->ownerName());
2917       if (!query.exec()) { // krazy:exclude=crashy
2918         qWarning("Error while saving ibanbic data for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text()));
2919         return false;
2920       }
2921       return true;
2922     };
2923 
2924     switch(action) {
2925       case SQLAction::Save:
2926         query.prepare("INSERT INTO kmmIbanBic "
2927                       " ( id, iban, bic, name )"
2928                       " VALUES( :id, :iban, :bic, :name ) "
2929                      );
2930         return writeQuery();
2931 
2932       case SQLAction::Modify:
2933         query.prepare("UPDATE kmmIbanBic SET iban = :iban, bic = :bic, name = :name WHERE id = :id;");
2934         return writeQuery();
2935 
2936       case SQLAction::Remove:
2937         query.prepare("DELETE FROM kmmIbanBic WHERE id = ?;");
2938         query.bindValue(0, obj.idString());
2939         if (!query.exec()) {
2940           qWarning("Error while deleting ibanbic data '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text()));
2941           return false;
2942         }
2943         return true;
2944     }
2945     return false;
2946   }
2947 
actOnNationalAccountObjectInSQL(SQLAction action,const payeeIdentifier & obj)2948   bool actOnNationalAccountObjectInSQL(SQLAction action, const payeeIdentifier &obj)
2949   {
2950     payeeIdentifierTyped<payeeIdentifiers::nationalAccount> payeeIdentifier = payeeIdentifierTyped<payeeIdentifiers::nationalAccount>(obj);
2951 
2952     Q_Q(MyMoneyStorageSql);
2953     QSqlQuery query(*q);
2954 
2955     auto writeQuery = [&]() {
2956       query.bindValue(":id", obj.idString());
2957       query.bindValue(":countryCode", payeeIdentifier->country());
2958       query.bindValue(":accountNumber", payeeIdentifier->accountNumber());
2959       query.bindValue(":bankCode", (payeeIdentifier->bankCode().isEmpty()) ? QVariant(QVariant::String) : payeeIdentifier->bankCode());
2960       query.bindValue(":name", payeeIdentifier->ownerName());
2961       if (!query.exec()) { // krazy:exclude=crashy
2962         qWarning("Error while saving national account number for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text()));
2963         return false;
2964       }
2965       return true;
2966 
2967     };
2968 
2969     switch(action) {
2970       case SQLAction::Save:
2971         query.prepare("INSERT INTO kmmNationalAccountNumber "
2972                       " ( id, countryCode, accountNumber, bankCode, name )"
2973                       " VALUES( :id, :countryCode, :accountNumber, :bankCode, :name ) "
2974                      );
2975         return writeQuery();
2976 
2977       case SQLAction::Modify:
2978         query.prepare("UPDATE kmmNationalAccountNumber SET countryCode = :countryCode, accountNumber = :accountNumber, bankCode = :bankCode, name = :name WHERE id = :id;");
2979         return writeQuery();
2980 
2981       case SQLAction::Remove:
2982         query.prepare("DELETE FROM kmmNationalAccountNumber WHERE id = ?;");
2983         query.bindValue(0, obj.idString());
2984         if (!query.exec()) {
2985           qWarning("Error while deleting national account number '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text()));
2986           return false;
2987         }
2988         return true;
2989     }
2990     return false;
2991   }
2992 
actOnSepaOnlineTransferObjectInSQL(SQLAction action,const onlineTask & obj,const QString & id)2993   bool actOnSepaOnlineTransferObjectInSQL(SQLAction action, const onlineTask &obj, const QString& id)
2994   {
2995     Q_Q(MyMoneyStorageSql);
2996     QSqlQuery query(*q);
2997     const auto& task = dynamic_cast<const sepaOnlineTransferImpl &>(obj);
2998 
2999     auto bindValuesToQuery = [&]() {
3000       auto value = task.value().toString();
3001       if (value.isEmpty()) {
3002         value = QStringLiteral("0");
3003       }
3004       query.bindValue(":id", id);
3005       query.bindValue(":originAccount", task.responsibleAccount());
3006       query.bindValue(":value", value);
3007       query.bindValue(":purpose", task.purpose());
3008       query.bindValue(":endToEndReference", (task.endToEndReference().isEmpty()) ? QVariant() : QVariant::fromValue(task.endToEndReference()));
3009       query.bindValue(":beneficiaryName", task.beneficiaryTyped().ownerName());
3010       query.bindValue(":beneficiaryIban", task.beneficiaryTyped().electronicIban());
3011       query.bindValue(":beneficiaryBic", (task.beneficiaryTyped().storedBic().isEmpty()) ? QVariant() : QVariant::fromValue(task.beneficiaryTyped().storedBic()));
3012       query.bindValue(":textKey", task.textKey());
3013       query.bindValue(":subTextKey", task.subTextKey());
3014     };
3015 
3016     switch(action) {
3017       case SQLAction::Save:
3018         query.prepare("INSERT INTO kmmSepaOrders ("
3019                       " id, originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, "
3020                       " beneficiaryBic, textKey, subTextKey) "
3021                       " VALUES( :id, :originAccount, :value, :purpose, :endToEndReference, :beneficiaryName, :beneficiaryIban, "
3022                       "         :beneficiaryBic, :textKey, :subTextKey ) "
3023                      );
3024         bindValuesToQuery();
3025         if (!query.exec()) {
3026           qWarning("Error while saving sepa order '%s': %s", qPrintable(id), qPrintable(query.lastError().text()));
3027           return false;
3028         }
3029         return true;
3030 
3031       case SQLAction::Modify:
3032         query.prepare(
3033           "UPDATE kmmSepaOrders SET"
3034           " originAccount = :originAccount,"
3035           " value = :value,"
3036           " purpose = :purpose,"
3037           " endToEndReference = :endToEndReference,"
3038           " beneficiaryName = :beneficiaryName,"
3039           " beneficiaryIban = :beneficiaryIban,"
3040           " beneficiaryBic = :beneficiaryBic,"
3041           " textKey = :textKey,"
3042           " subTextKey = :subTextKey "
3043           " WHERE id = :id");
3044         bindValuesToQuery();
3045         if (!query.exec()) {
3046           qWarning("Could not modify sepaOnlineTransfer '%s': %s", qPrintable(id), qPrintable(query.lastError().text()));
3047           return false;
3048         }
3049         return true;
3050 
3051       case SQLAction::Remove:
3052         query.prepare("DELETE FROM kmmSepaOrders WHERE id = ?");
3053         query.bindValue(0, id);
3054         return query.exec();
3055     }
3056     return false;
3057   }
3058 
actOnPayeeIdentifierObjectInSQL(SQLAction action,const payeeIdentifier & obj)3059   void actOnPayeeIdentifierObjectInSQL(SQLAction action, const payeeIdentifier& obj)
3060   {
3061     setupStoragePlugin(obj->payeeIdentifierId());
3062     auto isSuccessfull = false;
3063 
3064     if (obj->payeeIdentifierId() == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid())
3065       isSuccessfull = actOnIBANBICObjectInSQL(action, obj);
3066     else if (obj->payeeIdentifierId() == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid())
3067       isSuccessfull = actOnNationalAccountObjectInSQL(action, obj);
3068 
3069     if (!isSuccessfull) {
3070       switch (action) {
3071         case SQLAction::Save:
3072           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(obj.idString()));
3073         case SQLAction::Modify:
3074           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(obj.idString()));
3075         case SQLAction::Remove:
3076           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(obj.idString()));
3077       }
3078     }
3079   }
3080 
actOnOnlineJobInSQL(SQLAction action,const onlineTask & obj,const QString & id)3081   void actOnOnlineJobInSQL(SQLAction action, const onlineTask& obj, const QString& id)
3082   {
3083     setupStoragePlugin(obj.taskName());
3084     auto isSuccessfull = false;
3085 
3086     if (obj.taskName() == sepaOnlineTransferImpl::name())
3087       isSuccessfull = actOnSepaOnlineTransferObjectInSQL(action, obj, id);
3088 
3089     if (!isSuccessfull) {
3090       switch (action) {
3091         case SQLAction::Save:
3092           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(id));
3093         case SQLAction::Modify:
3094           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(id));
3095         case SQLAction::Remove:
3096           throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(id));
3097       }
3098     }
3099   }
3100 
createIBANBICObject(QSqlDatabase db,const QString & identId)3101   payeeIdentifierData* createIBANBICObject(QSqlDatabase db, const QString& identId) const
3102   {
3103     QSqlQuery query(db);
3104     query.prepare("SELECT iban, bic, name FROM kmmIbanBic WHERE id = ?;");
3105     query.bindValue(0, identId);
3106     if (!query.exec() || !query.next()) {
3107       qWarning("Could load iban bic identifier from database");
3108       return nullptr;
3109     }
3110 
3111     payeeIdentifiers::ibanBic *const ident = new payeeIdentifiers::ibanBic;
3112     ident->setIban(query.value(0).toString());
3113     ident->setBic(query.value(1).toString());
3114     ident->setOwnerName(query.value(2).toString());
3115     return ident;
3116   }
3117 
createNationalAccountObject(QSqlDatabase db,const QString & identId)3118   payeeIdentifierData* createNationalAccountObject(QSqlDatabase db, const QString& identId) const
3119   {
3120     QSqlQuery query(db);
3121     query.prepare("SELECT countryCode, accountNumber, bankCode, name FROM kmmNationalAccountNumber WHERE id = ?;");
3122     query.bindValue(0, identId);
3123     if (!query.exec() || !query.next()) {
3124       qWarning("Could load national account number from database");
3125       return nullptr;
3126     }
3127 
3128     payeeIdentifiers::nationalAccount *const ident = new payeeIdentifiers::nationalAccount;
3129     ident->setCountry(query.value(0).toString());
3130     ident->setAccountNumber(query.value(1).toString());
3131     ident->setBankCode(query.value(2).toString());
3132     ident->setOwnerName(query.value(3).toString());
3133     return ident;
3134   }
3135 
createPayeeIdentifierObject(QSqlDatabase db,const QString & identifierType,const QString & identifierId)3136   payeeIdentifier createPayeeIdentifierObject(QSqlDatabase db, const QString& identifierType, const QString& identifierId) const
3137   {
3138     payeeIdentifierData* identData = nullptr;
3139     if (identifierType == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid())
3140       identData = createIBANBICObject(db, identifierId);
3141     else if (identifierType == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid())
3142       identData = createNationalAccountObject(db, identifierId);
3143 
3144     return payeeIdentifier(identifierId, identData);
3145   }
3146 
createSepaOnlineTransferObject(QSqlDatabase connection,const QString & onlineJobId)3147   onlineTask* createSepaOnlineTransferObject(QSqlDatabase connection, const QString& onlineJobId) const
3148   {
3149     Q_ASSERT(!onlineJobId.isEmpty());
3150     Q_ASSERT(connection.isOpen());
3151 
3152     QSqlQuery query = QSqlQuery(
3153                         "SELECT originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, "
3154                         " beneficiaryBic, textKey, subTextKey FROM kmmSepaOrders WHERE id = ?",
3155                         connection
3156                       );
3157     query.bindValue(0, onlineJobId);
3158     if (query.exec() && query.next()) {
3159       sepaOnlineTransferImpl* task = new sepaOnlineTransferImpl();
3160       task->setOriginAccount(query.value(0).toString());
3161       task->setValue(MyMoneyMoney(query.value(1).toString()));
3162       task->setPurpose(query.value(2).toString());
3163       task->setEndToEndReference(query.value(3).toString());
3164       task->setTextKey(query.value(7).toUInt());
3165       task->setSubTextKey(query.value(8).toUInt());
3166 
3167       payeeIdentifiers::ibanBic beneficiary;
3168       beneficiary.setOwnerName(query.value(4).toString());
3169       beneficiary.setIban(query.value(5).toString());
3170       beneficiary.setBic(query.value(6).toString());
3171       task->setBeneficiary(beneficiary);
3172       return task;
3173     }
3174 
3175     return nullptr;
3176   }
3177 
createOnlineTaskObject(const QString & iid,const QString & onlineTaskId,QSqlDatabase connection)3178   onlineTask* createOnlineTaskObject(const QString& iid, const QString& onlineTaskId, QSqlDatabase connection) const
3179   {
3180     onlineTask* taskOnline = nullptr;
3181     if (iid == sepaOnlineTransferImpl::name()) {
3182       // @todo This is probably memory leak but for now it works alike to original code
3183       onlineJobAdministration::instance()->registerOnlineTask(new sepaOnlineTransferImpl);
3184       taskOnline = createSepaOnlineTransferObject(connection, onlineTaskId);
3185     }
3186     if (!taskOnline)
3187       qWarning("In the file is a onlineTask for which I could not find the plugin ('%s')", qPrintable(iid));
3188 
3189     return taskOnline;
3190   }
3191 
alert(QString s)3192   void alert(QString s) const // FIXME: remove...
3193   {
3194     qDebug() << s;
3195   }
3196 
signalProgress(qint64 current,qint64 total,const QString & msg)3197   void signalProgress(qint64 current, qint64 total, const QString& msg) const
3198   {
3199     if (m_progressCallback != 0)
3200       (*m_progressCallback)(current, total, msg);
3201   }
3202 
signalProgress(qint64 current,qint64 total)3203   void signalProgress(qint64 current, qint64 total) const
3204   {
3205     signalProgress(current, total, QString());
3206   }
3207 
3208   template<ulong MyMoneyStorageSqlPrivate::* cache>
getNextId(const QString & table,const QString & id,const int prefixLength)3209   ulong getNextId(const QString& table, const QString& id, const int prefixLength) const
3210   {
3211     Q_CHECK_PTR(cache);
3212     if (this->*cache == 0) {
3213       MyMoneyStorageSqlPrivate* nonConstThis = const_cast<MyMoneyStorageSqlPrivate*>(this);
3214       nonConstThis->*cache = 1 + nonConstThis->highestNumberFromIdString(table, id, prefixLength);
3215     }
3216     Q_ASSERT(this->*cache > 0); // everything else is never a valid id
3217     return this->*cache;
3218   }
3219 
3220   //void startCommitUnit (const QString& callingFunction);
3221   //void endCommitUnit (const QString& callingFunction);
3222   //void cancelCommitUnit (const QString& callingFunction);
3223 
3224   MyMoneyStorageSql *q_ptr;
3225 
3226   // data
3227   QExplicitlySharedDataPointer<MyMoneyDbDriver> m_driver;
3228 
3229   MyMoneyDbDef m_db;
3230   uint m_dbVersion;
3231   MyMoneyStorageMgr *m_storage;
3232   // input options
3233   bool m_loadAll; // preload all data
3234   bool m_override; // override open if already in use
3235   // error message
3236   QString m_error;
3237   // record counts
3238   ulong m_institutions;
3239   ulong m_accounts;
3240   ulong m_payees;
3241   ulong m_tags;
3242   ulong m_transactions;
3243   ulong m_splits;
3244   ulong m_securities;
3245   ulong m_prices;
3246   ulong m_currencies;
3247   ulong m_schedules;
3248   ulong m_reports;
3249   ulong m_kvps;
3250   ulong m_budgets;
3251   ulong m_onlineJobs;
3252   ulong m_payeeIdentifier;
3253 
3254   // Cache for next id to use
3255   // value 0 means data is not available and has to be loaded from the database
3256   ulong m_hiIdInstitutions;
3257   ulong m_hiIdPayees;
3258   ulong m_hiIdTags;
3259   ulong m_hiIdAccounts;
3260   ulong m_hiIdTransactions;
3261   ulong m_hiIdSchedules;
3262   ulong m_hiIdSecurities;
3263   ulong m_hiIdReports;
3264   ulong m_hiIdBudgets;
3265   ulong m_hiIdOnlineJobs;
3266   ulong m_hiIdPayeeIdentifier;
3267   ulong m_hiIdCostCenter;
3268 
3269   // encrypt option - usage TBD
3270   QString m_encryptData;
3271 
3272   /**
3273     * This variable is used to suppress status messages except during
3274    * initial data load and final write
3275 
3276   */
3277   bool m_displayStatus;
3278 
3279   /** The following keeps track of commitment units (known as transactions in SQL
3280     * though it would be confusing to use that term within KMM). It is implemented
3281     * as a stack for debug purposes. Long term, probably a count would suffice
3282     */
3283   QStack<QString> m_commitUnitStack;
3284   /**
3285     * This member variable is used to preload transactions for preferred accounts
3286     */
3287   MyMoneyTransactionFilter m_preferred;
3288   /**
3289     * This member variable is used because reading prices from a file uses the 'add...' function rather than a
3290     * 'load...' function which other objects use. Having this variable allows us to avoid needing to check the
3291     * database to see if this really is a new or modified price
3292     */
3293   bool m_readingPrices;
3294   /**
3295     * This member variable holds a map of transaction counts per account, indexed by
3296     * the account id. It is used
3297     * to avoid having to scan all transactions whenever a count is needed. It should
3298     * probably be moved into the MyMoneyAccount object; maybe we will do that once
3299     * the database code has been properly checked out
3300     */
3301   QHash<QString, ulong> m_transactionCountMap;
3302   /**
3303     * These member variables hold the user name and date/time of logon
3304     */
3305   QString m_logonUser;
3306   QDateTime m_logonAt;
3307   QDate m_txPostDate; // FIXME: remove when Tom puts date into split object
3308 
3309   bool m_newDatabase;
3310 
3311   /**
3312     * This member keeps the current precision to be used fro prices.
3313     * @sa setPrecision()
3314     */
3315   static int m_precision;
3316 
3317   /**
3318     * This member keeps the current start date used for transaction retrieval.
3319     * @sa setStartDate()
3320     */
3321   static QDate m_startDate;
3322 
3323   /**
3324     *
3325     */
3326   QSet<QString> m_loadedStoragePlugins;
3327 
3328   void (*m_progressCallback)(int, int, const QString&);
3329 };
3330 #endif
3331