summaryrefslogtreecommitdiffstats
path: root/kmymoney2/mymoney/storage/mymoneystoragesql.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'kmymoney2/mymoney/storage/mymoneystoragesql.cpp')
-rw-r--r--kmymoney2/mymoney/storage/mymoneystoragesql.cpp4511
1 files changed, 4511 insertions, 0 deletions
diff --git a/kmymoney2/mymoney/storage/mymoneystoragesql.cpp b/kmymoney2/mymoney/storage/mymoneystoragesql.cpp
new file mode 100644
index 0000000..97b4c55
--- /dev/null
+++ b/kmymoney2/mymoney/storage/mymoneystoragesql.cpp
@@ -0,0 +1,4511 @@
+/***************************************************************************
+ mymoneystoragesql.cpp
+ ---------------------
+ begin : 11 November 2005
+ copyright : (C) 2005 by Tony Bloomfield
+ email : tonybloom@users.sourceforge.net
+ : Fernando Vilas <fvilas@iname.com>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include <algorithm>
+#include <numeric>
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qstring.h>
+#include <qdatetime.h>
+#include <qvaluelist.h>
+#include <qstringlist.h>
+#include <qiodevice.h>
+#include <qsqldriver.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+#include <klocale.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "mymoneystoragesql.h"
+#include "imymoneyserialize.h"
+#include <kmymoney/kmymoneyglobalsettings.h>
+
+#define TRY try {
+#define CATCH } catch (MyMoneyException *e) {
+#define PASS } catch (MyMoneyException *e) { throw; }
+#define ECATCH }
+#define DBG(a) // qDebug(a)
+//#define TRACE(a) qDebug(a)
+#define TRACE(a) ::timetrace(a)
+
+//***************** THE CURRENT VERSION OF THE DATABASE LAYOUT ****************
+unsigned int MyMoneyDbDef::m_currentVersion = 6;
+
+// subclass QSqlQuery for performance tracing
+
+MyMoneySqlQuery::MyMoneySqlQuery (MyMoneyStorageSql* db)
+ : QSqlQuery (static_cast<QSqlDatabase*>(db)) {
+ m_db = db;
+}
+
+bool MyMoneySqlQuery::exec () {
+ TRACE(QString("start sql - %1").arg(lastQuery()));
+ bool rc = QSqlQuery::exec();
+ QString msg("end sql\n%1\n***Query returned %2, row count %3");
+ TRACE (msg.arg(QSqlQuery::executedQuery()).arg(rc).arg(numRowsAffected()));
+ //DBG (QString("%1\n***Query returned %2, row count %3").arg(QSqlQuery::executedQuery()).arg(rc).arg(size()));
+ return (rc);
+}
+
+bool MyMoneySqlQuery::prepare ( const QString & query ) {
+ if (m_db->isSqlite3()) {
+ QString newQuery = query;
+ return (QSqlQuery::prepare (newQuery.replace("FOR UPDATE", "")));
+ }
+ return (QSqlQuery::prepare (query));
+}
+
+//*****************************************************************************
+MyMoneyDbDrivers::MyMoneyDbDrivers () {
+ m_driverMap["QDB2"] = QString("IBM DB2");
+ m_driverMap["QIBASE"] = QString("Borland Interbase");
+ m_driverMap["QMYSQL3"] = QString("MySQL");
+ m_driverMap["QOCI8"] = QString("Oracle Call Interface, version 8 and 9");
+ m_driverMap["QODBC3"] = QString("Open Database Connectivity");
+ m_driverMap["QPSQL7"] = QString("PostgreSQL v6.x and v7.x");
+ m_driverMap["QTDS7"] = QString("Sybase Adaptive Server and Microsoft SQL Server");
+#if QT_VERSION < 0x040000
+ m_driverMap["QSQLITE3"] = QString("SQLite Version 3");
+#else
+ m_driverMap["QSQLITE"] = QString("SQLite Version 3");
+#endif
+}
+
+databaseTypeE MyMoneyDbDrivers::driverToType (const QString& driver) const {
+ if (driver == "QDB2") return(Db2);
+ else if (driver == "QIBASE") return(Interbase);
+ else if (driver == "QMYSQL3") return(Mysql);
+ else if (driver == "QOCI8") return(Oracle8);
+ else if (driver == "QODBC3") return(ODBC3);
+ else if (driver == "QPSQL7") return(Postgresql);
+ else if (driver == "QTDS7") return(Sybase);
+#if QT_VERSION < 0x040000
+ else if (driver == "QSQLITE3") return(Sqlite3);
+#else
+ else if (driver == "QSQLITE") return(Sqlite3);
+#endif
+ else throw new MYMONEYEXCEPTION (QString("Unknown database driver type").arg(driver));
+}
+
+bool MyMoneyDbDrivers::isTested (databaseTypeE dbType) const {
+ switch (dbType) {
+ case Mysql:
+ case Sqlite3:
+ case Postgresql:
+ return (true);
+ default:
+ return(false);
+ }
+ return(false);
+}
+
+//************************ Constructor/Destructor *****************************
+MyMoneyStorageSql::MyMoneyStorageSql (IMyMoneySerialize *storage, const KURL& url)
+ : QSqlDatabase (url.queryItem("driver"), QString("kmmdatabase")) {
+ DBG("*** Entering MyMoneyStorageSql::MyMoneyStorageSql");
+ m_dbVersion = 0;
+ m_progressCallback = 0;
+ m_displayStatus = false;
+ m_storage = storage;
+ m_storagePtr = dynamic_cast<IMyMoneyStorage*>(storage);
+ m_newDatabase = false;
+ m_readingPrices = false;
+ m_loadAll = false;
+ m_override = false;
+ m_preferred.setReportAllSplits(false);
+}
+
+int MyMoneyStorageSql::open(const KURL& url, int openMode, bool clear) {
+ DBG("*** Entering MyMoneyStorageSql::open");
+try {
+ int rc = 0;
+ QString driverName = url.queryItem("driver");
+ m_dbType = m_drivers.driverToType(driverName);
+ //get the input options
+ QStringList options = QStringList::split(',', url.queryItem("options"));
+ m_loadAll = options.contains("loadAll")/*|| m_mode == 0*/;
+ m_override = options.contains("override");
+
+ // create the database connection
+ QString dbName = url.path().right(url.path().length() - 1); // remove separator slash
+ setDatabaseName(dbName);
+ setHostName(url.host());
+ setUserName(url.user());
+ setPassword(url.pass());
+ switch (openMode) {
+ case IO_ReadOnly: // OpenDatabase menu entry (or open last file)
+ case IO_ReadWrite: // Save menu entry with database open
+ if (!QSqlDatabase::open()) {
+ buildError(MyMoneySqlQuery(), __func__, "opening database");
+ rc = 1;
+ } else {
+ rc = createTables(); // check all tables are present, create if not (we may add tables at some time)
+ }
+ break;
+ case IO_WriteOnly: // SaveAs Database - if exists, must be empty, if not will create
+ // Try to open the database.
+ // If that fails, try to create the database, then try to open it again.
+ m_newDatabase = true;
+ if (!QSqlDatabase::open()) {
+ if (createDatabase(url) != 0) {
+ rc = 1;
+ } else {
+ if (!QSqlDatabase::open()) {
+ buildError(MyMoneySqlQuery(), __func__, "opening new database");
+ rc = 1;
+ } else {
+ rc = createTables();
+ }
+ }
+ } else {
+ rc = createTables();
+ if (rc == 0) {
+ if (clear) {
+ clean();
+ } else {
+ rc = isEmpty();
+ }
+ }
+ }
+ break;
+ default:
+ qFatal("%s", QString("%1 - unknown open mode %2").arg(__func__).arg(openMode).data());
+ }
+ if (rc != 0) return (rc);
+ // bypass logon check if we are creating a database
+ if (openMode == IO_WriteOnly) return(0);
+ // check if the database is locked, if not lock it
+ readFileInfo();
+ if (!m_logonUser.isEmpty() && (!m_override)) {
+ m_error = QString
+ (i18n("Database apparently in use\nOpened by %1 on %2 at %3.\nOpen anyway?"))
+ .arg(m_logonUser)
+ .arg(m_logonAt.date().toString(Qt::ISODate))
+ .arg(m_logonAt.time().toString("hh.mm.ss"));
+ qDebug("%s", m_error.data());
+ close(false);
+ rc = -1;
+ } else {
+ m_logonUser = url.user() + "@" + url.host();
+ m_logonAt = QDateTime::currentDateTime();
+ writeFileInfo();
+ }
+ return(rc);
+} catch (QString& s) {
+ qDebug("%s",s.data());
+ return (1);
+}
+}
+
+void MyMoneyStorageSql::close(bool logoff) {
+ DBG("*** Entering MyMoneyStorageSql::close");
+ if (QSqlDatabase::open()) {
+ if (logoff) {
+ startCommitUnit(__func__);
+ m_logonUser = QString();
+ writeFileInfo();
+ endCommitUnit(__func__);
+ }
+ QSqlDatabase::close();
+ QSqlDatabase::removeDatabase(this);
+ }
+}
+
+int MyMoneyStorageSql::createDatabase (const KURL& url) {
+ DBG("*** Entering MyMoneyStorageSql::createDatabase");
+ if (m_dbType == Sqlite3) return(0); // not needed for sqlite
+ if (!m_dbType == Mysql) {
+ m_error =
+ QString(i18n("Cannot currently create database for driver %1; please create manually")).arg(driverName());
+ return (1);
+ }
+ // create the database (only works for mysql at present)
+ QString dbName = url.path().right(url.path().length() - 1); // remove separator slash
+ QSqlDatabase *maindb = QSqlDatabase::addDatabase(driverName());
+ maindb->setDatabaseName ("mysql");
+ maindb->setHostName (url.host());
+ maindb->setUserName (url.user());
+ maindb->setPassword (url.pass());
+ maindb->open();
+ QSqlQuery qm(maindb);
+ QString qs = QString("CREATE DATABASE %1;").arg(dbName);
+ qm.prepare (qs);
+ if (!qm.exec()) {
+ buildError (qm, __func__, QString(i18n("Error in create database %1; do you have create permissions?")).arg(dbName));
+ return (1);
+ }
+ QSqlDatabase::removeDatabase (maindb);
+ return (0);
+}
+
+
+int MyMoneyStorageSql::upgradeDb() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeDb");
+ //signalProgress(0, 1, QObject::tr("Upgrading database..."));
+ MyMoneySqlQuery q(this);
+ q.prepare ("SELECT version FROM kmmFileInfo;");
+ if (!q.exec() || !q.next()) {
+ if (!m_newDatabase) {
+ buildError (q, __func__, "Error retrieving file info(version)");
+ return(1);
+ } else {
+ m_dbVersion = m_db.currentVersion();
+ m_storage->setFileFixVersion(m_storage->currentFixVersion());
+ QSqlQuery q(this);
+ q.prepare("UPDATE kmmFileInfo SET version = :version, \
+ fixLevel = :fixLevel;");
+ q.bindValue(":version", m_dbVersion);
+ q.bindValue(":fixLevel", m_storage->currentFixVersion());
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating file info(version)");
+ return(1);
+ }
+ return (0);
+ }
+ }
+ // prior to dbv6, 'version' format was 'dbversion.fixLevel+1'
+ // as of dbv6, these are separate fields
+ QString version = q.value(0).toString();
+ if (version.contains('.')) {
+ m_dbVersion = q.value(0).toString().section('.', 0, 0).toUInt();
+ m_storage->setFileFixVersion(q.value(0).toString().section('.', 1, 1).toUInt() - 1);
+ } else {
+ m_dbVersion = version.toUInt();
+ q.prepare ("SELECT fixLevel FROM kmmFileInfo;");
+ if (!q.exec() || !q.next()) {
+ buildError (q, __func__, "Error retrieving file info (fixLevel)");
+ return(1);
+ }
+ m_storage->setFileFixVersion(q.value(0).toUInt());
+ }
+ int rc = 0;
+ while ((m_dbVersion < m_db.currentVersion()) && (rc == 0)) {
+ switch (m_dbVersion) {
+ case 0:
+ if ((rc = upgradeToV1()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 1:
+ if ((rc = upgradeToV2()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 2:
+ if ((rc = upgradeToV3()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 3:
+ if ((rc = upgradeToV4()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 4:
+ if ((rc = upgradeToV5()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 5:
+ if ((rc = upgradeToV6()) != 0) return (1);
+ ++m_dbVersion;
+ break;
+ case 6:
+ break;
+ default:
+ qFatal("Unknown version number in database - %d", m_dbVersion);
+ }
+ }
+ // write updated version to DB
+ //setVersion(QString("%1.%2").arg(m_dbVersion).arg(m_minorVersion));
+ q.prepare (QString("UPDATE kmmFileInfo SET version = :version;"));
+ q.bindValue(":version", m_dbVersion);
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating db version");
+ return (1);
+ }
+ //signalProgress(-1,-1);
+ return (0);
+}
+// SF bug 2779291
+// check whether a column appears in a table already; if not, add it
+bool MyMoneyStorageSql::addColumn
+ (const QString& table, const QString& col,
+ const QString& after)
+{
+ MyMoneyDbTable t = m_db.m_tables[table];
+ MyMoneyDbTable::field_iterator ft;
+ const MyMoneyDbColumn* c;
+ for (ft = t.begin(); ft != t.end(); ++ft) {
+ c = (*ft);
+ if (c->name() == col)
+ break;
+ }
+ if (ft == t.end()) qFatal("addColumn - get it right");
+ return (addColumn(t, *c, after));
+}
+
+bool MyMoneyStorageSql::addColumn
+ (const MyMoneyDbTable& t, const MyMoneyDbColumn& c,
+ const QString& after){
+ if ((m_dbType == Sqlite3) && (!after.isEmpty()))
+ qFatal("sqlite doesn't support 'AFTER'; use sqliteAlterTable");
+ if (record(t.name()).contains(c.name()))
+ return (true);
+ QSqlQuery q(this);
+ QString afterString = ";";
+ if (!after.isEmpty())
+ afterString = QString("AFTER %1;").arg(after);
+ q.prepare("ALTER TABLE " + t.name() + " ADD COLUMN " +
+ c.generateDDL(m_dbType) + afterString);
+ if (!q.exec()) {
+ buildError (q, __func__,
+ QString("Error adding column %1 to table %2").arg(c.name()).arg(t.name()));
+ return (false);
+ }
+ return (true);
+}
+
+// analogous to above
+bool MyMoneyStorageSql::dropColumn
+ (const QString& table, const QString& col)
+{
+ return (dropColumn(m_db.m_tables[table], col));
+}
+
+bool MyMoneyStorageSql::dropColumn
+ (const MyMoneyDbTable& t, const QString& col){
+ if (m_dbType == Sqlite3)
+ qFatal("sqlite doesn't support 'DROP COLUMN'; use sqliteAlterTable");
+ if (!record(t.name()).contains(col))
+ return (true);
+ QSqlQuery q(this);
+ q.prepare("ALTER TABLE " + t.name() + " DROP COLUMN "
+ + col + ";");
+ if (!q.exec()) {
+ buildError (q, __func__,
+ QString("Error dropping column %1 from table %2").arg(col).arg(t.name()));
+ return (false);
+ }
+ return (true);
+}
+
+int MyMoneyStorageSql::upgradeToV1() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV1");
+ if ((m_dbType == Sqlite) || (m_dbType == Sqlite3)) qFatal("SQLite upgrade NYI");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ // change kmmSplits pkey to (transactionId, splitId)
+ q.prepare ("ALTER TABLE kmmSplits ADD PRIMARY KEY (transactionId, splitId);");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating kmmSplits pkey");
+ return (1);
+ }
+ // change kmmSplits alter checkNumber varchar(32)
+ q.prepare (m_db.m_tables["kmmSplits"].modifyColumnString(m_dbType, "checkNumber",
+ MyMoneyDbColumn("checkNumber", "varchar(32)")));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error expanding kmmSplits.checkNumber");
+ return (1);
+ }
+ // change kmmSplits add postDate datetime
+ if (!addColumn(m_db.m_tables["kmmSplits"],
+ MyMoneyDbDatetimeColumn("postDate")))
+ return (1);
+ // initialize it to same value as transaction (do it the long way round)
+ q.prepare ("SELECT id, postDate FROM kmmTransactions WHERE txType = 'N';");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error priming kmmSplits.postDate");
+ return (1);
+ }
+ QMap<QString, QDateTime> tids;
+ while (q.next()) tids[q.value(0).toString()] = q.value(1).toDateTime();
+ QMap<QString, QDateTime>::ConstIterator it;
+ for (it = tids.begin(); it != tids.end(); ++it) {
+ q.prepare ("UPDATE kmmSplits SET postDate=:postDate WHERE transactionId = :id;");
+ q.bindValue(":postDate", it.data().toString(Qt::ISODate));
+ q.bindValue(":id", it.key());
+ if (!q.exec()) {
+ buildError (q, __func__, "priming kmmSplits.postDate");
+ return(1);
+ }
+ }
+ // add index to kmmKeyValuePairs to (kvpType,kvpId)
+ QStringList list;
+ list << "kvpType" << "kvpId";
+ q.prepare (MyMoneyDbIndex("kmmKeyValuePairs", "kmmKVPtype_id", list, false).generateDDL(m_dbType) + ";");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error adding kmmKeyValuePairs index");
+ return (1);
+ }
+ // add index to kmmSplits to (accountId, txType)
+ list.clear();
+ list << "accountId" << "txType";
+ q.prepare (MyMoneyDbIndex("kmmSplits", "kmmSplitsaccount_type", list, false).generateDDL(m_dbType) + ";");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error adding kmmSplits index");
+ return (1);
+ }
+ // change kmmSchedulePaymentHistory pkey to (schedId, payDate)
+ q.prepare ("ALTER TABLE kmmSchedulePaymentHistory ADD PRIMARY KEY (schedId, payDate);");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating kmmSchedulePaymentHistory pkey");
+ return (1);
+ }
+ // change kmmPrices pkey to (fromId, toId, priceDate)
+ q.prepare ("ALTER TABLE kmmPrices ADD PRIMARY KEY (fromId, toId, priceDate);");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating kmmPrices pkey");
+ return (1);
+ }
+ // change kmmReportConfig pkey to (name)
+ // There wasn't one previously, so no need to drop it.
+ q.prepare ("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (name);");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating kmmReportConfig pkey");
+ return (1);
+ }
+ // change kmmFileInfo add budgets unsigned bigint after kvps
+ if (!addColumn(m_db.m_tables["kmmFileInfo"],
+ MyMoneyDbIntColumn("budgets", MyMoneyDbIntColumn::BIG, false)))
+ return (1);
+ // change kmmFileInfo add hiBudgetId unsigned bigint after hiReportId
+ if (!addColumn(m_db.m_tables["kmmFileInfo"],
+ MyMoneyDbIntColumn("hiBudgetId", MyMoneyDbIntColumn::BIG, false)))
+ return (1);
+ // change kmmFileInfo add logonUser
+ if (!addColumn(m_db.m_tables["kmmFileInfo"],
+ MyMoneyDbColumn("logonUser", "varchar(255)", false)))
+ return (1);
+ // change kmmFileInfo add logonAt datetime
+ if (!addColumn(m_db.m_tables["kmmFileInfo"],
+ MyMoneyDbDatetimeColumn("logonAt", false)))
+ return (1);
+ // change kmmAccounts add transactionCount unsigned bigint as last field
+ if (!addColumn(m_db.m_tables["kmmAccounts"],
+ MyMoneyDbIntColumn("transactionCount", MyMoneyDbIntColumn::BIG, false)))
+ return (1);
+ // calculate the transaction counts. the application logic defines an account's tx count
+ // in such a way as to count multiple splits in a tx which reference the same account as one.
+ // this is the only way I can think of to do this which will work in sqlite too.
+ // inefficient, but it only gets done once...
+ // get a list of all accounts so we'll get a zero value for those without txs
+ q.prepare ("SELECT id FROM kmmAccounts");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error retrieving accounts for transaction counting");
+ return(1);
+ }
+ while (q.next()) {
+ m_transactionCountMap[q.value(0).toCString()] = 0;
+ }
+ q.prepare ("SELECT accountId, transactionId FROM kmmSplits WHERE txType = 'N' ORDER BY 1, 2");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error retrieving splits for transaction counting");
+ return(1);
+ }
+ QString lastAcc, lastTx;
+ while (q.next()) {
+ QString thisAcc = q.value(0).toCString();
+ QString thisTx = q.value(1).toCString();
+ if ((thisAcc != lastAcc) || (thisTx != lastTx)) ++m_transactionCountMap[thisAcc];
+ lastAcc = thisAcc;
+ lastTx = thisTx;
+ }
+ QMap<QString, unsigned long>::ConstIterator itm;
+ q.prepare("UPDATE kmmAccounts SET transactionCount = :txCount WHERE id = :id;");
+ for (itm = m_transactionCountMap.begin(); itm != m_transactionCountMap.end(); ++itm) {
+ q.bindValue (":txCount", QString::number(itm.data()));
+ q.bindValue (":id", itm.key());
+ if (!q.exec()) {
+ buildError(q, __func__, "Error updating transaction count");
+ return (1);
+ }
+ }
+ m_transactionCountMap.clear();
+ // there were considerable problems with record counts in V0, so rebuild them
+ readFileInfo();
+ m_institutions = getRecCount("kmmInstitutions");
+ m_accounts = getRecCount("kmmAccounts");
+ m_payees = getRecCount("kmmPayees");
+ m_transactions = getRecCount("kmmTransactions WHERE txType = 'N'");
+ m_splits = getRecCount("kmmSplits");
+ m_securities = getRecCount("kmmSecurities");
+ m_prices = getRecCount("kmmPrices");
+ m_currencies = getRecCount("kmmCurrencies");
+ m_schedules = getRecCount("kmmSchedules");
+ m_reports = getRecCount("kmmReportConfig");
+ m_kvps = getRecCount("kmmKeyValuePairs");
+ m_budgets = getRecCount("kmmBudgetConfig");
+ writeFileInfo();
+ /* if sqlite {
+ q.prepare("VACUUM;");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error vacuuming database");
+ return(1);
+ }
+ }*/
+ endCommitUnit(__func__);
+ return (0);
+}
+
+int MyMoneyStorageSql::upgradeToV2() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV2");
+ //SQLite3 now supports ALTER TABLE...ADD COLUMN, so only die if version < 3
+ //if (m_dbType == Sqlite3) qFatal("SQLite upgrade NYI");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ // change kmmSplits add price fields
+ if (!addColumn(m_db.m_tables["kmmSplits"],
+ MyMoneyDbTextColumn("price")))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmSplits"],
+ MyMoneyDbTextColumn("priceFormatted")))
+ return (1);
+ endCommitUnit(__func__);
+ return (0);
+}
+
+int MyMoneyStorageSql::upgradeToV3() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV3");
+ //SQLite3 now supports ALTER TABLE...ADD COLUMN, so only die if version < 3
+ //if (m_dbType == Sqlite3) qFatal("SQLite upgrade NYI");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ // The default value is given here to populate the column.
+ q.prepare ("ALTER TABLE kmmSchedules ADD COLUMN " +
+ MyMoneyDbIntColumn("occurenceMultiplier",
+ MyMoneyDbIntColumn::SMALL, false, false, true)
+ .generateDDL(m_dbType) + " DEFAULT 0;");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error adding kmmSchedules.occurenceMultiplier");
+ return (1);
+ }
+ //The default is less than any useful value, so as each schedule is hit, it will update
+ //itself to the appropriate value.
+ endCommitUnit(__func__);
+ return 0;
+}
+
+int MyMoneyStorageSql::upgradeToV4() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV4");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ QStringList list;
+ list << "transactionId" << "splitId";
+ q.prepare (MyMoneyDbIndex("kmmSplits", "kmmTx_Split", list, false).generateDDL(m_dbType) + ";");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error adding kmmSplits index on (transactionId, splitId)");
+ return (1);
+ }
+ endCommitUnit(__func__);
+ return 0;
+}
+
+int MyMoneyStorageSql::upgradeToV5() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV5");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ if (!addColumn(m_db.m_tables["kmmSplits"],
+ MyMoneyDbTextColumn("bankId")))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmPayees"],
+ MyMoneyDbTextColumn("notes", MyMoneyDbTextColumn::LONG)))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmPayees"],
+ MyMoneyDbColumn("defaultAccountId", "varchar(32)")))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmPayees"],
+ MyMoneyDbIntColumn("matchData", MyMoneyDbIntColumn::TINY,
+ false)))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmPayees"],
+ MyMoneyDbColumn("matchIgnoreCase", "char(1)")))
+ return (1);
+ if (!addColumn(m_db.m_tables["kmmPayees"],
+ MyMoneyDbTextColumn("matchKeys")))
+ return (1);
+ const MyMoneyDbTable& t = m_db.m_tables["kmmReportConfig"];
+ if (m_dbType != Sqlite3) {
+ q.prepare (t.dropPrimaryKeyString(m_dbType));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error dropping Report table keys");
+ return (1);
+ }
+ } else {
+ if (!sqliteAlterTable(t))
+ return (1);
+ }
+ endCommitUnit(__func__);
+ return 0;
+}
+
+int MyMoneyStorageSql::upgradeToV6() {
+ DBG("*** Entering MyMoneyStorageSql::upgradeToV6");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ // add separate fix level in file info
+ if (!addColumn("kmmFileInfo", "fixLevel"))
+ return (1);
+ // upgrade Mysql to InnoDB transaction-safe engine
+ if (m_dbType == Mysql) {
+ for (QMapConstIterator<QString, MyMoneyDbTable> tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) {
+ q.prepare(QString("ALTER TABLE %1 ENGINE = InnoDB;").arg(tt.data().name()));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating to InnoDB");
+ return (1);
+ }
+ }
+ }
+ // add unique id to reports table
+ if (!addColumn(m_db.m_tables["kmmReportConfig"],
+ MyMoneyDbColumn("id", "varchar(32)")))
+ return(1);
+ // read and write reports to get ids inserted
+ readFileInfo();
+ QMap<QString, MyMoneyReport> reportList =
+ fetchReports();
+ // the V5 database allowed lots of duplicate reports with no
+ // way to distinguish between them. The fetchReports call
+ // will have effectively removed all duplicates
+ // so we now delete from the db and re-write them
+ q.prepare("DELETE FROM kmmReportConfig;");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error deleting reports");
+ return (1);
+ }
+ unsigned long long hiReportId = 0;
+ QMap<QString, MyMoneyReport>::const_iterator it_r;
+ for(it_r = reportList.begin(); it_r != reportList.end(); ++it_r) {
+ MyMoneyReport r = *it_r;
+ hiReportId = calcHighId(hiReportId, r.id());
+ q.prepare (m_db.m_tables["kmmReportConfig"].insertString());
+ writeReport(*it_r, q);
+ }
+ m_hiIdReports = hiReportId;
+ m_storage->loadReportId(m_hiIdReports);
+ // sqlite3 doesn't support ADD PRIMARY KEY
+ if (m_dbType == Sqlite3) {
+ if (!sqliteAlterTable(m_db.m_tables["kmmReportConfig"])) {
+ return (1);
+ }
+ } else {
+ q.prepare ("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (id);");
+ if (!q.exec()) {
+ buildError (q, __func__, "Error updating kmmReportConfig pkey");
+ return (1);
+ }
+ }
+ endCommitUnit(__func__);
+ return 0;
+}
+
+/* This function attempts to cater for limitations in the sqlite ALTER TABLE
+ statement. It should enable us to drop a primary key, and drop columns */
+bool MyMoneyStorageSql::sqliteAlterTable(const MyMoneyDbTable& t) {
+ DBG("*** Entering MyMoneyStorageSql::sqliteAlterTable");
+ QString tempTableName = t.name();
+ tempTableName.replace("kmm", "tmp");
+ QSqlQuery q(this);
+ q.prepare (QString("ALTER TABLE " + t.name() + " RENAME TO " + tempTableName + ";"));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error renaming table");
+ return false;
+ }
+ createTable(t);
+ q.prepare (QString("INSERT INTO " + t.name() + " (" + t.columnList() +
+ ") SELECT " + t.columnList() + " FROM " + tempTableName + ";"));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error inserting into new table");
+ return false;
+ }
+ q.prepare (QString("DROP TABLE " + tempTableName + ";"));
+ if (!q.exec()) {
+ buildError (q, __func__, "Error dropping old table");
+ return false;
+ }
+ return true;
+}
+
+long unsigned MyMoneyStorageSql::getRecCount (const QString& table) const {
+ DBG("*** Entering MyMoneyStorageSql::getRecCount");
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare(QString("SELECT COUNT(*) FROM %1;").arg(table));
+ if ((!q.exec()) || (!q.next())) {
+ buildError (q, __func__, "error retrieving record count");
+ qFatal("Error retrieving record count"); // definitely shouldn't happen
+ }
+ return ((unsigned long) q.value(0).toULongLong());
+}
+
+int MyMoneyStorageSql::createTables () {
+ DBG("*** Entering MyMoneyStorageSql::createTables");
+ // check tables, create if required
+ // convert everything to lower case, since SQL standard is case insensitive
+ // table and column names (when not delimited), but some DBMSs disagree.
+ QStringList lowerTables = tables(QSql::AllTables);
+ for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) {
+ (*i) = (*i).lower();
+ }
+
+ for (QMapConstIterator<QString, MyMoneyDbTable> tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) {
+ if (!lowerTables.contains(tt.key().lower())) createTable (tt.data());
+ }
+
+ MyMoneySqlQuery q(this);
+ for (QMapConstIterator<QString, MyMoneyDbView> tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) {
+ if (!lowerTables.contains(tt.key().lower())) {
+ q.prepare (tt.data().createString());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString ("creating view %1").arg(tt.key())));
+ }
+ }
+
+ // get the current db version from kmmFileInfo.
+ // upgrade if necessary.
+
+ return (upgradeDb()); // any errors will be caught by exception handling
+}
+
+void MyMoneyStorageSql::createTable (const MyMoneyDbTable& t) {
+ DBG("*** Entering MyMoneyStorageSql::createTable");
+// create the tables
+ QStringList ql = QStringList::split('\n', t.generateCreateSQL(m_dbType));
+ MyMoneySqlQuery q(this);
+ for (unsigned int i = 0; i < ql.count(); ++i) {
+ q.prepare (ql[i]);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString ("creating table/index %1").arg(t.name())));
+ }
+}
+
+int MyMoneyStorageSql::isEmpty () {
+ DBG("*** Entering MyMoneyStorageSql::isEmpty");
+ // check all tables are empty
+ QMapConstIterator<QString, MyMoneyDbTable> tt = m_db.tableBegin();
+ int recordCount = 0;
+ MyMoneySqlQuery q(this);
+ while ((tt != m_db.tableEnd()) && (recordCount == 0)) {
+ q.prepare (QString("select count(*) from %1;").arg((*tt).name()));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "getting record count"));
+ if (!q.next()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "retrieving record count"));
+ recordCount += q.value(0).toInt();
+ ++tt;
+ }
+
+ if (recordCount != 0) {
+ return (-1); // not empty
+ } else {
+ return (0);
+ }
+}
+
+void MyMoneyStorageSql::clean() {
+ DBG("*** Entering MyMoneyStorageSql::clean");
+// delete all existing records
+ QMapConstIterator<QString, MyMoneyDbTable> it = m_db.tableBegin();
+ MyMoneySqlQuery q(this);
+ while (it != m_db.tableEnd()) {
+ q.prepare(QString("DELETE from %1;").arg(it.key()));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString ("cleaning database")));
+ ++it;
+ }
+}
+
+//////////////////////////////////////////////////////////////////
+
+bool MyMoneyStorageSql::readFile(void) {
+ DBG("*** Entering MyMoneyStorageSql::readFile");
+ m_displayStatus = true;
+ try {
+ readFileInfo();
+ readInstitutions();
+ if (m_loadAll) {
+ readPayees();
+ } else {
+ QValueList<QString> user;
+ user.append(QString("USER"));
+ readPayees(user);
+ }
+ //TRACE("done payees");
+ readCurrencies();
+ //TRACE("done currencies");
+ readSecurities();
+ //TRACE("done securities");
+ readAccounts();
+ if (m_loadAll) {
+ readTransactions();
+ } else {
+ if (m_preferred.filterSet().singleFilter.accountFilter) readTransactions (m_preferred);
+ }
+ //TRACE("done accounts");
+ readSchedules();
+ //TRACE("done schedules");
+ readPrices();
+ //TRACE("done prices");
+ readReports();
+ //TRACE("done reports");
+ readBudgets();
+ //TRACE("done budgets");
+ //FIXME - ?? if (m_mode == 0)
+ //m_storage->rebuildAccountBalances();
+ // this seems to be nonsense, but it clears the dirty flag
+ // as a side-effect.
+ m_storage->setLastModificationDate(m_storage->lastModificationDate());
+ // FIXME?? if (m_mode == 0) m_storage = NULL;
+ // make sure the progress bar is not shown any longer
+ signalProgress(-1, -1);
+ m_displayStatus = false;
+ //MyMoneySqlQuery::traceOn();
+ return true;
+ } catch (QString& s) {
+ return false;
+ }
+}
+
+// The following is called from 'SaveAsDatabase'
+bool MyMoneyStorageSql::writeFile(void) {
+ DBG("*** Entering MyMoneyStorageSql::writeFile");
+ // initialize record counts and hi ids
+ m_institutions = m_accounts = m_payees = m_transactions = m_splits
+ = m_securities = m_prices = m_currencies = m_schedules = m_reports = m_kvps = m_budgets = 0;
+ m_hiIdInstitutions = m_hiIdPayees = m_hiIdAccounts = m_hiIdTransactions =
+ m_hiIdSchedules = m_hiIdSecurities = m_hiIdReports = m_hiIdBudgets = 0;
+ m_displayStatus = true;
+ try{
+ startCommitUnit(__func__);
+ writeInstitutions ();
+ writePayees();
+ writeAccounts();
+ writeTransactions();
+ writeSchedules();
+ writeSecurities();
+ writePrices();
+ writeCurrencies();
+ writeReports();
+ writeBudgets();
+ writeFileInfo();
+ // this seems to be nonsense, but it clears the dirty flag
+ // as a side-effect.
+ //m_storage->setLastModificationDate(m_storage->lastModificationDate());
+ // FIXME?? if (m_mode == 0) m_storage = NULL;
+ endCommitUnit(__func__);
+ // make sure the progress bar is not shown any longer
+ signalProgress(-1, -1);
+ m_displayStatus = false;
+ return true;
+} catch (QString& s) {
+ return false;
+}
+}
+// --------------- SQL Transaction (commit unit) handling -----------------------------------
+void MyMoneyStorageSql::startCommitUnit (const QString& callingFunction) {
+ DBG("*** Entering MyMoneyStorageSql::startCommitUnit");
+ if (m_commitUnitStack.isEmpty()) {
+ if (!transaction()) throw new MYMONEYEXCEPTION(buildError (MyMoneySqlQuery(), __func__, "starting commit unit"));
+ }
+ m_commitUnitStack.push(callingFunction);
+}
+
+bool MyMoneyStorageSql::endCommitUnit (const QString& callingFunction) {
+ DBG("*** Entering MyMoneyStorageSql::endCommitUnit");
+ // for now, we don't know if there were any changes made to the data so
+ // we expect the data to have changed. This assumption causes some unnecessary
+ // repaints of the UI here and there, but for now it's ok. If we can determine
+ // that the commit() really changes the data, we can return that information
+ // as value of this method.
+ bool rc = true;
+ if (callingFunction != m_commitUnitStack.top())
+ qDebug("%s", QString("%1 - %2 s/be %3").arg(__func__).arg(callingFunction).arg(m_commitUnitStack.top()).data());
+ m_commitUnitStack.pop();
+ if (m_commitUnitStack.isEmpty()) {
+ if (!commit()) throw new MYMONEYEXCEPTION(buildError (MyMoneySqlQuery(), __func__, "ending commit unit"));
+ }
+ return rc;
+}
+
+void MyMoneyStorageSql::cancelCommitUnit (const QString& callingFunction) {
+ DBG("*** Entering MyMoneyStorageSql::cancelCommitUnit");
+ if (callingFunction != m_commitUnitStack.top())
+ qDebug("%s", QString("%1 - %2 s/be %3").arg(__func__).arg(callingFunction).arg(m_commitUnitStack.top()).data());
+ if (m_commitUnitStack.isEmpty()) return;
+ m_commitUnitStack.clear();
+ if (!rollback()) throw new MYMONEYEXCEPTION(buildError (MyMoneySqlQuery(), __func__, "cancelling commit unit"));
+}
+
+/////////////////////////////////////////////////////////////////////
+void MyMoneyStorageSql::fillStorage() {
+ DBG("*** Entering MyMoneyStorageSql::fillStorage");
+// if (!m_transactionListRead) // make sure we have loaded everything
+ readTransactions();
+// if (!m_payeeListRead)
+ readPayees();
+}
+
+//------------------------------ Write SQL routines ----------------------------------------
+// **** Institutions ****
+void MyMoneyStorageSql::writeInstitutions() {
+ DBG("*** Entering MyMoneyStorageSql::writeInstitutions");
+ // first, get a list of what's on the database
+ // anything not in the list needs to be inserted
+ // anything which is will be updated and removed from the list
+ // anything left over at the end will need to be deleted
+ // this is an expensive and inconvenient way to do things; find a better way
+ // one way would be to build the lists when reading the db
+ // unfortunately this object does not persist between read and write
+ // it would also be nice if we could tell which objects had been updated since we read them in
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT id FROM kmmInstitutions;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Institution list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ const QValueList<MyMoneyInstitution> list = m_storage->institutionList();
+ QValueList<MyMoneyInstitution>::ConstIterator it;
+ MyMoneySqlQuery q2(this);
+ q.prepare (m_db.m_tables["kmmInstitutions"].updateString());
+ q2.prepare (m_db.m_tables["kmmInstitutions"].insertString());
+ signalProgress(0, list.count(), "Writing Institutions...");
+ for(it = list.begin(); it != list.end(); ++it) {
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeInstitution(*it, q);
+ } else {
+ writeInstitution(*it, q2);
+ }
+ signalProgress (++m_institutions, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ QValueList<QString>::const_iterator it = dbList.begin();
+ q.prepare("DELETE FROM kmmInstitutions WHERE id = :id");
+ while (it != dbList.end()) {
+ q.bindValue(":id", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Institution"));
+ deleteKeyValuePairs("OFXSETTINGS", (*it));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addInstitution(const MyMoneyInstitution& inst) {
+ DBG("*** Entering MyMoneyStorageSql::addInstitution");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmInstitutions"].insertString());
+ writeInstitution(inst ,q);
+ ++m_institutions;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyInstitution(const MyMoneyInstitution& inst) {
+ DBG("*** Entering MyMoneyStorageSql::modifyInstitution");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmInstitutions"].updateString());
+ deleteKeyValuePairs("OFXSETTINGS", inst.id());
+ writeInstitution(inst ,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeInstitution(const MyMoneyInstitution& inst) {
+ DBG("*** Entering MyMoneyStorageSql::removeInstitution");
+ startCommitUnit(__func__);
+ deleteKeyValuePairs("OFXSETTINGS", inst.id());
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmInstitutions"].deleteString());
+ q.bindValue(":id", inst.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Institution")));
+ --m_institutions;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeInstitution(const MyMoneyInstitution& i, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeInstitution");
+ q.bindValue(":id", i.id());
+ q.bindValue(":name", i.name());
+ q.bindValue(":manager", i.manager());
+ q.bindValue(":routingCode", i.sortcode());
+ q.bindValue(":addressStreet", i.street());
+ q.bindValue(":addressCity", i.city());
+ q.bindValue(":addressZipcode", i.postcode());
+ q.bindValue(":telephone", i.telephone());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Institution")));
+ writeKeyValuePairs("OFXSETTINGS", i.id(), i.pairs());
+ m_hiIdInstitutions = calcHighId(m_hiIdInstitutions, i.id());
+}
+
+// **** Payees ****
+void MyMoneyStorageSql::writePayees() {
+ DBG("*** Entering MyMoneyStorageSql::writePayees");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT id FROM kmmPayees;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Payee list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ QValueList<MyMoneyPayee> list = m_storage->payeeList();
+ MyMoneyPayee user(QString("USER"), m_storage->user());
+ list.prepend(user);
+ signalProgress(0, list.count(), "Writing Payees...");
+ MyMoneySqlQuery q2(this);
+ q.prepare (m_db.m_tables["kmmPayees"].updateString());
+ q2.prepare (m_db.m_tables["kmmPayees"].insertString());
+ QValueList<MyMoneyPayee>::ConstIterator it;
+ for(it = list.begin(); it != list.end(); ++it) {
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writePayee(*it, q);
+ } else {
+ writePayee(*it, q2);
+ }
+ signalProgress(++m_payees, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ QValueList<QString>::const_iterator it = dbList.begin();
+ q.prepare(m_db.m_tables["kmmPayees"].deleteString());
+ while (it != dbList.end()) {
+ q.bindValue(":id", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Payee"));
+ m_payees -= q.numRowsAffected();
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addPayee(const MyMoneyPayee& payee) {
+ DBG("*** Entering MyMoneyStorageSql::addPayee");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPayees"].insertString());
+ writePayee(payee,q);
+ ++m_payees;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyPayee(const MyMoneyPayee& payee) {
+ DBG("*** Entering MyMoneyStorageSql::modifyPayee");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPayees"].updateString());
+ writePayee(payee,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyUserInfo(const MyMoneyPayee& payee) {
+ DBG("*** Entering MyMoneyStorageSql::modifyUserInfo");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPayees"].updateString());
+ writePayee(payee,q, true);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removePayee(const MyMoneyPayee& payee) {
+ DBG("*** Entering MyMoneyStorageSql::removePayee");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPayees"].deleteString());
+ q.bindValue(":id", payee.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Payee")));
+ --m_payees;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writePayee(const MyMoneyPayee& p, MyMoneySqlQuery& q, bool isUserInfo) {
+ DBG("*** Entering MyMoneyStorageSql::writePayee");
+ if (isUserInfo) {
+ q.bindValue(":id", "USER");
+ } else {
+ q.bindValue(":id", p.id());
+ }
+ q.bindValue(":name", p.name());
+ q.bindValue(":reference", p.reference());
+ q.bindValue(":email", p.email());
+ q.bindValue(":addressStreet", p.address());
+ q.bindValue(":addressCity", p.city());
+ q.bindValue(":addressZipcode", p.postcode());
+ q.bindValue(":addressState", p.state());
+ q.bindValue(":telephone", p.telephone());
+ q.bindValue(":notes", p.notes());
+ q.bindValue(":defaultAccountId", p.defaultAccountId());
+ bool ignoreCase;
+ QString matchKeys;
+ MyMoneyPayee::payeeMatchType type = p.matchData(ignoreCase, matchKeys);
+ q.bindValue(":matchData", static_cast<unsigned int>(type));
+ if (ignoreCase) q.bindValue(":matchIgnoreCase", "Y");
+ else q.bindValue(":matchIgnoreCase", "N");
+ q.bindValue(":matchKeys", matchKeys);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString ("writing Payee")));
+ if (!isUserInfo) m_hiIdPayees = calcHighId(m_hiIdPayees, p.id());
+}
+
+// **** Accounts ****
+void MyMoneyStorageSql::writeAccounts() {
+ DBG("*** Entering MyMoneyStorageSql::writeAccounts");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT id FROM kmmAccounts;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Account list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ QValueList<MyMoneyAccount> list;
+ m_storage->accountList(list);
+ QValueList<MyMoneyAccount>::ConstIterator it;
+ signalProgress(0, list.count(), "Writing Accounts...");
+ if (dbList.isEmpty()) { // new table, insert standard accounts
+ q.prepare (m_db.m_tables["kmmAccounts"].insertString());
+ } else {
+ q.prepare (m_db.m_tables["kmmAccounts"].updateString());
+ }
+ // Attempt to write the standard accounts. For an empty db, this will fail.
+ TRY
+ writeAccount(m_storage->asset(), q); ++m_accounts;
+ writeAccount(m_storage->liability(), q); ++m_accounts;
+ writeAccount(m_storage->expense(), q); ++m_accounts;
+ writeAccount(m_storage->income(), q); ++m_accounts;
+ writeAccount(m_storage->equity(), q); ++m_accounts;
+ CATCH
+ delete e;
+
+ // If the above failed, assume that the database is empty and create
+ // the standard accounts by hand before writing them.
+ MyMoneyAccount acc_l;
+ acc_l.setAccountType(MyMoneyAccount::Liability);
+ acc_l.setName("Liability");
+ MyMoneyAccount liability(STD_ACC_LIABILITY, acc_l);
+
+ MyMoneyAccount acc_a;
+ acc_a.setAccountType(MyMoneyAccount::Asset);
+ acc_a.setName("Asset");
+ MyMoneyAccount asset(STD_ACC_ASSET, acc_a);
+
+ MyMoneyAccount acc_e;
+ acc_e.setAccountType(MyMoneyAccount::Expense);
+ acc_e.setName("Expense");
+ MyMoneyAccount expense(STD_ACC_EXPENSE, acc_e);
+
+ MyMoneyAccount acc_i;
+ acc_i.setAccountType(MyMoneyAccount::Income);
+ acc_i.setName("Income");
+ MyMoneyAccount income(STD_ACC_INCOME, acc_i);
+
+ MyMoneyAccount acc_q;
+ acc_q.setAccountType(MyMoneyAccount::Equity);
+ acc_q.setName("Equity");
+ MyMoneyAccount equity(STD_ACC_EQUITY, acc_q);
+
+ writeAccount(asset, q); ++m_accounts;
+ writeAccount(expense, q); ++m_accounts;
+ writeAccount(income, q); ++m_accounts;
+ writeAccount(liability, q); ++m_accounts;
+ writeAccount(equity, q); ++m_accounts;
+ ECATCH
+
+ int i = 0;
+ MyMoneySqlQuery q2(this);
+ q.prepare (m_db.m_tables["kmmAccounts"].updateString());
+ q2.prepare (m_db.m_tables["kmmAccounts"].insertString());
+ // Update the accounts that exist; insert the ones that do not.
+ for(it = list.begin(); it != list.end(); ++it, ++i) {
+ m_transactionCountMap[(*it).id()] = m_storagePtr->transactionCount((*it).id());
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeAccount(*it, q);
+ } else {
+ writeAccount(*it, q2);
+ }
+ signalProgress(++m_accounts, 0);
+ }
+
+ // Delete the accounts that are in the db but no longer in memory.
+ if (!dbList.isEmpty()) {
+ QValueList<QString>::const_iterator it = dbList.begin();
+ q.prepare("DELETE FROM kmmAccounts WHERE id = :id");
+ while (it != dbList.end()) {
+ if (!m_storagePtr->isStandardAccount(*it)) {
+ q.bindValue(":id", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Account"));
+ deleteKeyValuePairs("ACCOUNT", (*it));
+ deleteKeyValuePairs("ONLINEBANKING", (*it));
+ }
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addAccount(const MyMoneyAccount& acc) {
+ DBG("*** Entering MyMoneyStorageSql::addAccount");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmAccounts"].insertString());
+ writeAccount(acc,q);
+ ++m_accounts;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyAccount(const MyMoneyAccount& acc) {
+ DBG("*** Entering MyMoneyStorageSql::modifyAccount");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmAccounts"].updateString());
+ deleteKeyValuePairs("ACCOUNT", acc.id());
+ deleteKeyValuePairs("ONLINEBANKING", acc.id());
+ writeAccount(acc,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeAccount(const MyMoneyAccount& acc) {
+ DBG("*** Entering MyMoneyStorageSql::removeAccount");
+ startCommitUnit(__func__);
+ deleteKeyValuePairs("ACCOUNT", acc.id());
+ deleteKeyValuePairs("ONLINEBANKING", acc.id());
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmAccounts"].deleteString());
+ q.bindValue(":id", acc.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Account")));
+ --m_accounts;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeAccount(const MyMoneyAccount& acc, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeAccount");
+ //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate());
+ q.bindValue(":id", acc.id());
+ q.bindValue(":institutionId", acc.institutionId());
+ q.bindValue(":parentId", acc.parentAccountId());
+ if (acc.lastReconciliationDate() == QDate())
+ q.bindValue(":lastReconciled", acc.lastReconciliationDate());
+ else
+ q.bindValue(":lastReconciled", acc.lastReconciliationDate().toString(Qt::ISODate));
+
+ q.bindValue(":lastModified", acc.lastModified());
+ if (acc.openingDate() == QDate())
+ q.bindValue(":openingDate", acc.openingDate());
+ else
+ q.bindValue(":openingDate", acc.openingDate().toString(Qt::ISODate));
+
+ q.bindValue(":accountNumber", acc.number());
+ q.bindValue(":accountType", acc.accountType());
+ q.bindValue(":accountTypeString", MyMoneyAccount::accountTypeToString(acc.accountType()));
+ if (acc.accountType() == MyMoneyAccount::Stock) {
+ q.bindValue(":isStockAccount", "Y");
+ } else {
+ q.bindValue(":isStockAccount", "N");
+ }
+ q.bindValue(":accountName", acc.name());
+ q.bindValue(":description", acc.description());
+ q.bindValue(":currencyId", acc.currencyId());
+
+ // This section attempts to get the balance from the database, if possible
+ // That way, the balance fields are kept in sync. If that fails, then
+ // It is assumed that the account actually knows its correct balance.
+
+ //FIXME: Using exceptions for branching always feels like a kludge.
+ // Look for a better way.
+ TRY
+ MyMoneyMoney bal = m_storagePtr->balance(acc.id(), QDate());
+ q.bindValue(":balance", bal.toString());
+ q.bindValue(":balanceFormatted",
+ bal.formatMoney("", -1, false));
+ CATCH
+ delete e;
+ q.bindValue(":balance", acc.balance().toString());
+ q.bindValue(":balanceFormatted",
+ acc.balance().formatMoney("", -1, false));
+ ECATCH
+
+ q.bindValue(":transactionCount", Q_ULLONG(m_transactionCountMap[acc.id()]));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Account")));
+
+ //Add in Key-Value Pairs for accounts.
+ //MMAccount inherits from KVPContainer AND has a KVPContainer member
+ //so handle both
+ writeKeyValuePairs("ACCOUNT", acc.id(), acc.pairs());
+ writeKeyValuePairs("ONLINEBANKING", acc.id(), acc.onlineBankingSettings().pairs());
+ m_hiIdAccounts = calcHighId(m_hiIdAccounts, acc.id());
+}
+
+// **** Transactions and Splits ****
+void MyMoneyStorageSql::writeTransactions() {
+ DBG("*** Entering MyMoneyStorageSql::writeTransactions");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT id FROM kmmTransactions WHERE txType = 'N';");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Transaction list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ MyMoneyTransactionFilter filter;
+ filter.setReportAllSplits(false);
+ QValueList<MyMoneyTransaction> list;
+ m_storage->transactionList(list, filter);
+ signalProgress(0, list.count(), "Writing Transactions...");
+ QValueList<MyMoneyTransaction>::ConstIterator it;
+ int i = 0;
+ MyMoneySqlQuery q2(this);
+ q.prepare (m_db.m_tables["kmmTransactions"].updateString());
+ q2.prepare (m_db.m_tables["kmmTransactions"].insertString());
+ for(it = list.begin(); it != list.end(); ++it, ++i) {
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeTransaction((*it).id(), *it, q, "N");
+ } else {
+ writeTransaction((*it).id(), *it, q2, "N");
+ }
+ signalProgress(++m_transactions, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ deleteTransaction(*it);
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addTransaction (const MyMoneyTransaction& tx) {
+ DBG("*** Entering MyMoneyStorageSql::addTransaction");
+ startCommitUnit(__func__);
+ // add the transaction and splits
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmTransactions"].insertString());
+ writeTransaction(tx.id(), tx, q, "N");
+ ++m_transactions;
+ // for each split account, update lastMod date, balance, txCount
+ QValueList<MyMoneySplit>::ConstIterator it_s;
+ for(it_s = tx.splits().begin(); it_s != tx.splits().end(); ++it_s) {
+ //MyMoneyAccount acc = m_storagePtr->account((*it_s).accountId());
+ MyMoneyAccount acc = MyMoneyFile::instance()->account((*it_s).accountId());
+ ++m_transactionCountMap[acc.id()];
+ modifyAccount(acc);
+ }
+ // in the fileinfo record, update lastMod, txCount, next TxId
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyTransaction (const MyMoneyTransaction& tx) {
+ DBG("*** Entering MyMoneyStorageSql::modifyTransaction");
+ startCommitUnit(__func__);
+ // remove the splits of the old tx from the count table
+ MyMoneySqlQuery q(this);
+ q.prepare ("SELECT accountId FROM kmmSplits WHERE transactionId = :txId;");
+ q.bindValue(":txId", tx.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "retrieving old splits"));
+ while (q.next()) {
+ QString id = q.value(0).toCString();
+ --m_transactionCountMap[id];
+ }
+ // add the transaction and splits
+ q.prepare (m_db.m_tables["kmmTransactions"].updateString());
+ writeTransaction(tx.id(), tx, q, "N");
+ // for each split account, update lastMod date, balance, txCount
+ QValueList<MyMoneySplit>::ConstIterator it_s;
+ for(it_s = tx.splits().begin(); it_s != tx.splits().end(); ++it_s) {
+ //MyMoneyAccount acc = m_storagePtr->account((*it_s).accountId());
+ MyMoneyAccount acc = MyMoneyFile::instance()->account((*it_s).accountId());
+ ++m_transactionCountMap[acc.id()];
+ modifyAccount(acc);
+ }
+ writeSplits(tx.id(), "N", tx.splits());
+ // in the fileinfo record, update lastMod
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeTransaction(const MyMoneyTransaction& tx) {
+ DBG("*** Entering MyMoneyStorageSql::removeTransaction");
+ startCommitUnit(__func__);
+ deleteTransaction(tx.id());
+ --m_transactions;
+
+ // for each split account, update lastMod date, balance, txCount
+ QValueList<MyMoneySplit>::ConstIterator it_s;
+ for(it_s = tx.splits().begin(); it_s != tx.splits().end(); ++it_s) {
+ MyMoneyAccount acc = m_storagePtr->account((*it_s).accountId());
+ --m_transactionCountMap[acc.id()];
+ modifyAccount(acc);
+ }
+ // in the fileinfo record, update lastModDate, txCount
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::deleteTransaction(const QString& id) {
+ DBG("*** Entering MyMoneyStorageSql::deleteTransaction");
+ MyMoneySqlQuery q(this);
+ q.prepare("DELETE FROM kmmSplits WHERE transactionId = :transactionId;");
+ q.bindValue(":transactionId", id);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Splits"));
+
+ q.prepare ("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' "
+ "AND kvpId LIKE '" + id + "%'");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Splits KVP"));
+
+ m_splits -= q.numRowsAffected();
+ deleteKeyValuePairs("TRANSACTION", id);
+ q.prepare(m_db.m_tables["kmmTransactions"].deleteString());
+ q.bindValue(":id", id);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Transaction"));
+}
+
+void MyMoneyStorageSql::writeTransaction(const QString& txId, const MyMoneyTransaction& tx, MyMoneySqlQuery& q, const QString& type) {
+ DBG("*** Entering MyMoneyStorageSql::writeTransaction");
+ q.bindValue(":id", txId);
+ q.bindValue(":txType", type);
+ q.bindValue(":postDate", tx.postDate().toString(Qt::ISODate));
+ q.bindValue(":memo", tx.memo());
+ q.bindValue(":entryDate", tx.entryDate().toString(Qt::ISODate));
+ q.bindValue(":currencyId", tx.commodity());
+ q.bindValue(":bankId", tx.bankID());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Transaction")));
+
+ m_txPostDate = tx.postDate(); // FIXME: TEMP till Tom puts date in split object
+ QValueList<MyMoneySplit> splitList = tx.splits();
+ writeSplits(txId, type, splitList);
+
+ //Add in Key-Value Pairs for transactions.
+ deleteKeyValuePairs("TRANSACTION", txId);
+ writeKeyValuePairs("TRANSACTION", txId, tx.pairs());
+ m_hiIdTransactions = calcHighId(m_hiIdTransactions, tx.id());
+}
+
+void MyMoneyStorageSql::writeSplits(const QString& txId, const QString& type, const QValueList<MyMoneySplit>& splitList) {
+ DBG("*** Entering MyMoneyStorageSql::writeSplits");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<unsigned int> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT splitId FROM kmmSplits where transactionId = :id;");
+ q.bindValue(":id", txId);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Split list"));
+ while (q.next()) dbList.append(q.value(0).toUInt());
+
+ QValueList<MyMoneySplit>::const_iterator it;
+ unsigned int i;
+ MyMoneySqlQuery q2(this);
+ q.prepare (m_db.m_tables["kmmSplits"].updateString());
+ q2.prepare (m_db.m_tables["kmmSplits"].insertString());
+ for(it = splitList.begin(), i = 0; it != splitList.end(); ++it, ++i) {
+ if (dbList.contains(i)) {
+ dbList.remove (i);
+ writeSplit(txId, (*it), type, i, q);
+ } else {
+ ++m_splits;
+ writeSplit(txId, (*it), type, i, q2);
+ }
+ }
+
+ if (!dbList.isEmpty()) {
+ q.prepare("DELETE FROM kmmSplits WHERE transactionId = :txId AND splitId = :splitId");
+ QValueList<unsigned int>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ q.bindValue(":txId", txId);
+ q.bindValue(":splitId", *it);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Splits"));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::writeSplit(const QString& txId, const MyMoneySplit& split,
+ const QString& type, const int splitId, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeSplit");
+ q.bindValue(":transactionId", txId);
+ q.bindValue(":txType", type);
+ q.bindValue(":splitId", splitId);
+ q.bindValue(":payeeId", split.payeeId());
+ if (split.reconcileDate() == QDate())
+ q.bindValue(":reconcileDate", split.reconcileDate());
+ else
+ q.bindValue(":reconcileDate", split.reconcileDate().toString(Qt::ISODate));
+ q.bindValue(":action", split.action());
+ q.bindValue(":reconcileFlag", split.reconcileFlag());
+ q.bindValue(":value", split.value().toString());
+ q.bindValue(":valueFormatted", split.value()
+ .formatMoney("", -1, false)
+ .replace(QChar(','), QChar('.')));
+ q.bindValue(":shares", split.shares().toString());
+ MyMoneyAccount acc = m_storagePtr->account(split.accountId());
+ MyMoneySecurity sec = m_storagePtr->security(acc.currencyId());
+ q.bindValue(":sharesFormatted",
+ split.shares().
+ formatMoney("", MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()), false).
+ replace(QChar(','), QChar('.')));
+ MyMoneyMoney price = split.actualPrice();
+ if (!price.isZero()) {
+ q.bindValue(":price", price.toString());
+ q.bindValue(":priceFormatted", price.formatMoney
+ ("", KMyMoneySettings::pricePrecision(), false)
+ .replace(QChar(','), QChar('.')));
+ } else {
+ q.bindValue(":price", QString());
+ q.bindValue(":priceFormatted", QString());
+ }
+ q.bindValue(":memo", split.memo());
+ q.bindValue(":accountId", split.accountId());
+ q.bindValue(":checkNumber", split.number());
+ q.bindValue(":postDate", m_txPostDate.toString(Qt::ISODate)); // FIXME: when Tom puts date into split object
+ q.bindValue(":bankId", split.bankID());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Split")));
+ deleteKeyValuePairs("SPLIT", txId + QString::number(splitId));
+ writeKeyValuePairs("SPLIT", txId + QString::number(splitId), split.pairs());
+}
+
+// **** Schedules ****
+void MyMoneyStorageSql::writeSchedules() {
+ DBG("*** Entering MyMoneyStorageSql::writeSchedules");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ q.prepare("SELECT id FROM kmmSchedules;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Schedule list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ const QValueList<MyMoneySchedule> list = m_storage->scheduleList();
+ QValueList<MyMoneySchedule>::ConstIterator it;
+ MyMoneySqlQuery q2(this);
+ //TODO: find a way to prepare the queries outside of the loop. writeSchedule()
+ // modifies the query passed to it, so they have to be re-prepared every pass.
+ signalProgress(0, list.count(), "Writing Schedules...");
+ for(it = list.begin(); it != list.end(); ++it) {
+ q.prepare (m_db.m_tables["kmmSchedules"].updateString());
+ q2.prepare (m_db.m_tables["kmmSchedules"].insertString());
+ bool insert = true;
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ insert = false;
+ writeSchedule(*it, q, insert);
+ } else {
+ writeSchedule(*it, q2, insert);
+ }
+ signalProgress(++m_schedules, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ deleteSchedule(*it);
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addSchedule(const MyMoneySchedule& sched) {
+ DBG("*** Entering MyMoneyStorageSql::addSchedule");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmSchedules"].insertString());
+ writeSchedule(sched,q, true);
+ ++m_schedules;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifySchedule(const MyMoneySchedule& sched) {
+ DBG("*** Entering MyMoneyStorageSql::modifySchedule");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmSchedules"].updateString());
+ writeSchedule(sched,q, false);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeSchedule(const MyMoneySchedule& sched) {
+ DBG("*** Entering MyMoneyStorageSql::removeSchedule");
+ startCommitUnit(__func__);
+ deleteSchedule(sched.id());
+ --m_schedules;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::deleteSchedule (const QString& id) {
+ DBG("*** Entering MyMoneyStorageSql::deleteSchedule");
+ deleteTransaction(id);
+ MyMoneySqlQuery q(this);
+ q.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id");
+ q.bindValue(":id", id);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Schedule Payment History"));
+ q.prepare(m_db.m_tables["kmmSchedules"].deleteString());
+ q.bindValue(":id", id);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Schedule"));
+ //FIXME: enable when schedules have KVPs.
+ //deleteKeyValuePairs("SCHEDULE", id);
+}
+
+void MyMoneyStorageSql::writeSchedule(const MyMoneySchedule& sch, MyMoneySqlQuery& q, bool insert) {
+ DBG("*** Entering MyMoneyStorageSql::writeSchedule");
+ q.bindValue(":id", sch.id());
+ q.bindValue(":name", sch.name());
+ q.bindValue(":type", sch.type());
+ q.bindValue(":typeString", MyMoneySchedule::scheduleTypeToString(sch.type()));
+ q.bindValue(":occurence", sch.occurencePeriod());
+ q.bindValue(":occurenceMultiplier", sch.occurenceMultiplier());
+ q.bindValue(":occurenceString", sch.occurenceToString());
+ q.bindValue(":paymentType", sch.paymentType());
+ q.bindValue(":paymentTypeString", MyMoneySchedule::paymentMethodToString(sch.paymentType()));
+ q.bindValue(":startDate", sch.startDate().toString(Qt::ISODate));
+ q.bindValue(":endDate", sch.endDate().toString(Qt::ISODate));
+ if (sch.isFixed()) {
+ q.bindValue(":fixed", "Y");
+ } else {
+ q.bindValue(":fixed", "N");
+ }
+ if (sch.autoEnter()) {
+ q.bindValue(":autoEnter", "Y");
+ } else {
+ q.bindValue(":autoEnter", "N");
+ }
+ q.bindValue(":lastPayment", sch.lastPayment());
+ q.bindValue(":nextPaymentDue", sch.nextDueDate().toString(Qt::ISODate));
+ q.bindValue(":weekendOption", sch.weekendOption());
+ q.bindValue(":weekendOptionString", MyMoneySchedule::weekendOptionToString(sch.weekendOption()));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Schedules")));
+
+ //store the payment history for this scheduled task.
+ //easiest way is to delete all and re-insert; it's not a high use table
+ q.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id;");
+ q.bindValue(":id", sch.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Schedule Payment History")));
+
+ q.prepare (m_db.m_tables["kmmSchedulePaymentHistory"].insertString());
+ QValueList<QDate> payments = sch.recordedPayments();
+ QValueList<QDate>::ConstIterator it;
+ for (it=payments.begin(); it!=payments.end(); ++it) {
+ q.bindValue(":schedId", sch.id());
+ q.bindValue(":payDate", (*it).toString(Qt::ISODate));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Schedule Payment History")));
+ }
+
+ //store the transaction data for this task.
+ if (!insert) {
+ q.prepare (m_db.m_tables["kmmTransactions"].updateString());
+ } else {
+ q.prepare (m_db.m_tables["kmmTransactions"].insertString());
+ }
+ writeTransaction(sch.id(), sch.transaction(), q, "S");
+
+ //FIXME: enable when schedules have KVPs.
+
+ //Add in Key-Value Pairs for transactions.
+ //deleteKeyValuePairs("SCHEDULE", sch.id());
+ //writeKeyValuePairs("SCHEDULE", sch.id(), sch.pairs());
+ m_hiIdSchedules = calcHighId(m_hiIdSchedules, sch.id());
+}
+
+// **** Securities ****
+void MyMoneyStorageSql::writeSecurities() {
+ DBG("*** Entering MyMoneyStorageSql::writeSecurities");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ MyMoneySqlQuery q2(this);
+ q.prepare("SELECT id FROM kmmSecurities;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building security list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ const QValueList<MyMoneySecurity> securityList = m_storage->securityList();
+ signalProgress(0, securityList.count(), "Writing Securities...");
+ q.prepare (m_db.m_tables["kmmSecurities"].updateString());
+ q2.prepare (m_db.m_tables["kmmSecurities"].insertString());
+ for(QValueList<MyMoneySecurity>::ConstIterator it = securityList.begin(); it != securityList.end(); ++it) {
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeSecurity((*it), q);
+ } else {
+ writeSecurity((*it), q2);
+ }
+ signalProgress(++m_securities, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ q.prepare("DELETE FROM kmmSecurities WHERE id = :id");
+ q2.prepare("DELETE FROM kmmPrices WHERE fromId = :id OR toId = :id");
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ q.bindValue(":id", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Security"));
+ q2.bindValue(":fromId", (*it));
+ q2.bindValue(":toId", (*it));
+ if (!q2.exec()) throw new MYMONEYEXCEPTION(buildError (q2, __func__, "deleting Security"));
+ deleteKeyValuePairs("SECURITY", (*it));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addSecurity(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::addSecurity");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmSecurities"].insertString());
+ writeSecurity(sec,q);
+ ++m_securities;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifySecurity(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::modifySecurity");
+ startCommitUnit(__func__);
+ deleteKeyValuePairs("SECURITY", sec.id());
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmSecurities"].updateString());
+ writeSecurity(sec,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeSecurity(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::removeSecurity");
+ startCommitUnit(__func__);
+ deleteKeyValuePairs("SECURITY", sec.id());
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmSecurities"].deleteString());
+ q.bindValue(":id", sec.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Security")));
+ --m_securities;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeSecurity(const MyMoneySecurity& security, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeSecurity");
+ q.bindValue(":id", security.id());
+ q.bindValue(":name", security.name());
+ q.bindValue(":symbol", security.tradingSymbol());
+ q.bindValue(":type", static_cast<int>(security.securityType()));
+ q.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType()));
+ q.bindValue(":smallestAccountFraction", security.smallestAccountFraction());
+ q.bindValue(":tradingCurrency", security.tradingCurrency());
+ q.bindValue(":tradingMarket", security.tradingMarket());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString ("writing Securities")));
+
+ //Add in Key-Value Pairs for security
+ writeKeyValuePairs("SECURITY", security.id(), security.pairs());
+ m_hiIdSecurities = calcHighId(m_hiIdSecurities, security.id());
+}
+
+// **** Prices ****
+void MyMoneyStorageSql::writePrices() {
+ DBG("*** Entering MyMoneyStorageSql::writePrices");
+ // due to difficulties in matching and determining deletes
+ // easiest way is to delete all and re-insert
+ MyMoneySqlQuery q(this);
+ q.prepare("DELETE FROM kmmPrices");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Prices")));
+ m_prices = 0;
+
+ const MyMoneyPriceList list = m_storage->priceList();
+ signalProgress(0, list.count(), "Writing Prices...");
+ MyMoneyPriceList::ConstIterator it;
+ for(it = list.begin(); it != list.end(); ++it) {
+ writePricePair(*it);
+ }
+}
+
+void MyMoneyStorageSql::writePricePair(const MyMoneyPriceEntries& p) {
+ DBG("*** Entering MyMoneyStorageSql::writePricePair");
+ MyMoneyPriceEntries::ConstIterator it;
+ for(it = p.begin(); it != p.end(); ++it) {
+ writePrice (*it);
+ signalProgress(++m_prices, 0);
+ }
+}
+
+void MyMoneyStorageSql::addPrice(const MyMoneyPrice& p) {
+ DBG("*** Entering MyMoneyStorageSql::addPrice");
+ if (m_readingPrices) return;
+ // the app always calls addPrice, whether or not there is already one there
+ startCommitUnit(__func__);
+ bool newRecord = false;
+ MyMoneySqlQuery q(this);
+ QString s = m_db.m_tables["kmmPrices"].selectAllString(false);
+ s += " WHERE fromId = :fromId AND toId = :toId AND priceDate = :priceDate;";
+ q.prepare (s);
+ q.bindValue(":fromId", p.from());
+ q.bindValue(":toId", p.to());
+ q.bindValue(":priceDate", p.date().toString(Qt::ISODate));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("finding Price")));
+ if (q.next()) {
+ q.prepare(m_db.m_tables["kmmPrices"].updateString());
+ } else {
+ q.prepare(m_db.m_tables["kmmPrices"].insertString());
+ ++m_prices;
+ newRecord = true;
+ }
+ q.bindValue(":fromId", p.from());
+ q.bindValue(":toId", p.to());
+ q.bindValue(":priceDate", p.date().toString(Qt::ISODate));
+ q.bindValue(":price", p.rate(QString()).toString());
+ q.bindValue(":priceFormatted",
+ p.rate(QString()).formatMoney("", KMyMoneySettings::pricePrecision()));
+ q.bindValue(":priceSource", p.source());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Price")));
+
+ if (newRecord) writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removePrice(const MyMoneyPrice& p) {
+ DBG("*** Entering MyMoneyStorageSql::removePrice");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPrices"].deleteString());
+ q.bindValue(":fromId", p.from());
+ q.bindValue(":toId", p.to());
+ q.bindValue(":priceDate", p.date().toString(Qt::ISODate));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Price")));
+ --m_prices;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writePrice(const MyMoneyPrice& p) {
+ DBG("*** Entering MyMoneyStorageSql::writePrice");
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmPrices"].insertString());
+ q.bindValue(":fromId", p.from());
+ q.bindValue(":toId", p.to());
+ q.bindValue(":priceDate", p.date().toString(Qt::ISODate));
+ q.bindValue(":price", p.rate(QString()).toString());
+ q.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", 2));
+ q.bindValue(":priceSource", p.source());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Prices")));
+}
+
+// **** Currencies ****
+void MyMoneyStorageSql::writeCurrencies() {
+ DBG("*** Entering MyMoneyStorageSql::writeCurrencies");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ MyMoneySqlQuery q2(this);
+ q.prepare("SELECT ISOCode FROM kmmCurrencies;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Currency list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ const QValueList<MyMoneySecurity> currencyList = m_storage->currencyList();
+ signalProgress(0, currencyList.count(), "Writing Currencies...");
+ q.prepare (m_db.m_tables["kmmCurrencies"].updateString());
+ q2.prepare (m_db.m_tables["kmmCurrencies"].insertString());
+ for(QValueList<MyMoneySecurity>::ConstIterator it = currencyList.begin(); it != currencyList.end(); ++it) {
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeCurrency((*it), q);
+ } else {
+ writeCurrency((*it), q2);
+ }
+ signalProgress(++m_currencies, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ q.prepare("DELETE FROM kmmCurrencies WHERE ISOCode = :ISOCode");
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ q.bindValue(":ISOCode", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Currency"));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addCurrency(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::addCurrency");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmCurrencies"].insertString());
+ writeCurrency(sec,q);
+ ++m_currencies;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyCurrency(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::modifyCurrency");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmCurrencies"].updateString());
+ writeCurrency(sec,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeCurrency(const MyMoneySecurity& sec) {
+ DBG("*** Entering MyMoneyStorageSql::removeCurrency");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmCurrencies"].deleteString());
+ q.bindValue(":ISOcode", sec.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Currency")));
+ --m_currencies;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeCurrency(const MyMoneySecurity& currency, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeCurrency");
+ q.bindValue(":ISOcode", currency.id());
+ q.bindValue(":name", currency.name());
+ q.bindValue(":type", static_cast<int>(currency.securityType()));
+ q.bindValue(":typeString", MyMoneySecurity::securityTypeToString(currency.securityType()));
+ // writing the symbol as three short ints is a PITA, but the
+ // problem is that database drivers have incompatible ways of declaring UTF8
+ QString symbol = currency.tradingSymbol() + " ";
+ q.bindValue(":symbol1", symbol.mid(0,1).unicode()->unicode());
+ q.bindValue(":symbol2", symbol.mid(1,1).unicode()->unicode());
+ q.bindValue(":symbol3", symbol.mid(2,1).unicode()->unicode());
+ q.bindValue(":symbolString", symbol);
+ q.bindValue(":partsPerUnit", currency.partsPerUnit());
+ q.bindValue(":smallestCashFraction", currency.smallestCashFraction());
+ q.bindValue(":smallestAccountFraction", currency.smallestAccountFraction());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Currencies")));
+}
+
+
+void MyMoneyStorageSql::writeReports() {
+ DBG("*** Entering MyMoneyStorageSql::writeReports");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ MyMoneySqlQuery q2(this);
+ q.prepare("SELECT id FROM kmmReportConfig;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Report list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ QValueList<MyMoneyReport> list = m_storage->reportList();
+ signalProgress(0, list.count(), "Writing Reports...");
+ QValueList<MyMoneyReport>::ConstIterator it;
+ q.prepare (m_db.m_tables["kmmReportConfig"].updateString());
+ q2.prepare (m_db.m_tables["kmmReportConfig"].insertString());
+ for(it = list.begin(); it != list.end(); ++it){
+ if (dbList.contains((*it).id())) {
+ dbList.remove ((*it).id());
+ writeReport(*it, q);
+ } else {
+ writeReport(*it, q2);
+ }
+ signalProgress(++m_reports, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ q.prepare("DELETE FROM kmmReportConfig WHERE id = :id");
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ q.bindValue(":id", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Report"));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addReport(const MyMoneyReport& rep) {
+ DBG("*** Entering MyMoneyStorageSql::addReport");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmReportConfig"].insertString());
+ writeReport(rep,q);
+ ++m_reports;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyReport(const MyMoneyReport& rep) {
+ DBG("*** Entering MyMoneyStorageSql::modifyReport");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmReportConfig"].updateString());
+ writeReport(rep,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeReport(const MyMoneyReport& rep) {
+ DBG("*** Entering MyMoneyStorageSql::removeReport");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare("DELETE FROM kmmReportConfig WHERE id = :id");
+ q.bindValue(":id", rep.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Report")));
+ --m_reports;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeReport (const MyMoneyReport& rep, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeReport");
+ QDomDocument d; // create a dummy XML document
+ QDomElement e = d.createElement("REPORTS");
+ d.appendChild (e);
+ rep.writeXML(d, e); // write the XML to document
+ q.bindValue(":id", rep.id());
+ q.bindValue(":name", rep.name());
+ q.bindValue(":XML", d.toString());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Reports")));
+ //m_hiIdReports = calcHighId(m_hiIdReports, rep.id());
+}
+
+void MyMoneyStorageSql::writeBudgets() {
+ DBG("*** Entering MyMoneyStorageSql::writeBudgets");
+ // first, get a list of what's on the database (see writeInstitutions)
+ QValueList<QString> dbList;
+ MyMoneySqlQuery q(this);
+ MyMoneySqlQuery q2(this);
+ q.prepare("SELECT name FROM kmmBudgetConfig;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "building Budget list"));
+ while (q.next()) dbList.append(q.value(0).toString());
+
+ QValueList<MyMoneyBudget> list = m_storage->budgetList();
+ signalProgress(0, list.count(), "Writing Budgets...");
+ QValueList<MyMoneyBudget>::ConstIterator it;
+ q.prepare (m_db.m_tables["kmmBudgetConfig"].updateString());
+ q2.prepare (m_db.m_tables["kmmBudgetConfig"].insertString());
+ for(it = list.begin(); it != list.end(); ++it){
+ if (dbList.contains((*it).name())) {
+ dbList.remove ((*it).name());
+ writeBudget(*it, q);
+ } else {
+ writeBudget(*it, q2);
+ }
+ signalProgress(++m_budgets, 0);
+ }
+
+ if (!dbList.isEmpty()) {
+ q.prepare("DELETE FROM kmmBudgetConfig WHERE id = :id");
+ QValueList<QString>::const_iterator it = dbList.begin();
+ while (it != dbList.end()) {
+ q.bindValue(":name", (*it));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "deleting Budget"));
+ ++it;
+ }
+ }
+}
+
+void MyMoneyStorageSql::addBudget(const MyMoneyBudget& bud) {
+ DBG("*** Entering MyMoneyStorageSql::addBudget");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmBudgetConfig"].insertString());
+ writeBudget(bud,q);
+ ++m_budgets;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::modifyBudget(const MyMoneyBudget& bud) {
+ DBG("*** Entering MyMoneyStorageSql::modifyBudget");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmBudgetConfig"].updateString());
+ writeBudget(bud,q);
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::removeBudget(const MyMoneyBudget& bud) {
+ DBG("*** Entering MyMoneyStorageSql::removeBudget");
+ startCommitUnit(__func__);
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmBudgetConfig"].deleteString());
+ q.bindValue(":id", bud.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting Budget")));
+ --m_budgets;
+ writeFileInfo();
+ endCommitUnit(__func__);
+}
+
+void MyMoneyStorageSql::writeBudget (const MyMoneyBudget& bud, MyMoneySqlQuery& q) {
+ DBG("*** Entering MyMoneyStorageSql::writeBudget");
+ QDomDocument d; // create a dummy XML document
+ QDomElement e = d.createElement("BUDGETS");
+ d.appendChild (e);
+ bud.writeXML(d, e); // write the XML to document
+ q.bindValue(":id", bud.id());
+ q.bindValue(":name", bud.name());
+ q.bindValue(":start", bud.budgetStart());
+ q.bindValue(":XML", d.toString());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing Budgets")));
+}
+
+void MyMoneyStorageSql::writeFileInfo() {
+ DBG("*** Entering MyMoneyStorageSql::writeFileInfo");
+ // we have no real way of knowing when these change, so re-write them every time
+ deleteKeyValuePairs("STORAGE", "");
+ writeKeyValuePairs("STORAGE", "", m_storage->pairs());
+ //
+ MyMoneySqlQuery q(this);
+ q.prepare ("SELECT * FROM kmmFileInfo;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, "checking fileinfo"));
+ QString qs;
+ if (q.next())
+ qs = m_db.m_tables["kmmFileInfo"].updateString();
+ else
+ qs = (m_db.m_tables["kmmFileInfo"].insertString());
+ q.prepare(qs);
+ q.bindValue(":version", m_dbVersion);
+ q.bindValue(":fixLevel", m_storage->fileFixVersion());
+ q.bindValue(":created", m_storage->creationDate().toString(Qt::ISODate));
+ //q.bindValue(":lastModified", m_storage->lastModificationDate().toString(Qt::ISODate));
+ q.bindValue(":lastModified", QDate::currentDate().toString(Qt::ISODate));
+ q.bindValue(":baseCurrency", m_storage->pairs()["kmm-baseCurrency"]);
+ q.bindValue(":institutions", (unsigned long long) m_institutions);
+ q.bindValue(":accounts", (unsigned long long) m_accounts);
+ q.bindValue(":payees", (unsigned long long) m_payees);
+ q.bindValue(":transactions", (unsigned long long) m_transactions);
+ q.bindValue(":splits", (unsigned long long) m_splits);
+ q.bindValue(":securities", (unsigned long long) m_securities);
+ q.bindValue(":prices", (unsigned long long) m_prices);
+ q.bindValue(":currencies", (unsigned long long) m_currencies);
+ q.bindValue(":schedules", (unsigned long long) m_schedules);
+ q.bindValue(":reports", (unsigned long long) m_reports);
+ q.bindValue(":kvps", (unsigned long long) m_kvps);
+ q.bindValue(":budgets", (unsigned long long) m_budgets);
+ q.bindValue(":dateRangeStart", QDate());
+ q.bindValue(":dateRangeEnd", QDate());
+
+ //FIXME: This modifies all m_<variable> used in this function.
+ // Sometimes the memory has been updated.
+
+ // Should most of these be tracked in a view?
+ // Variables actually needed are: version, fileFixVersion, creationDate,
+ // baseCurrency, encryption, update info, and logon info.
+ try {
+ //readFileInfo();
+ } catch (...) {
+ startCommitUnit(__func__);
+ }
+
+ q.bindValue(":hiInstitutionId", (unsigned long long) m_hiIdInstitutions);
+ q.bindValue(":hiPayeeId", (unsigned long long) m_hiIdPayees);
+ q.bindValue(":hiAccountId", (unsigned long long) m_hiIdAccounts);
+ q.bindValue(":hiTransactionId", (unsigned long long) m_hiIdTransactions);
+ q.bindValue(":hiScheduleId", (unsigned long long) m_hiIdSchedules);
+ q.bindValue(":hiSecurityId", (unsigned long long) m_hiIdSecurities);
+ q.bindValue(":hiReportId", (unsigned long long) m_hiIdReports);
+ q.bindValue(":hiBudgetId", (unsigned long long) m_hiIdBudgets);
+
+ q.bindValue(":encryptData", m_encryptData);
+ q.bindValue(":updateInProgress", "N");
+ q.bindValue(":logonUser", m_logonUser);
+ q.bindValue(":logonAt", m_logonAt.toString(Qt::ISODate));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing FileInfo")));
+}
+
+// **** Key/value pairs ****
+void MyMoneyStorageSql::writeKeyValuePairs(const QString& kvpType, const QString& kvpId, const QMap<QString, QString>& pairs) {
+ DBG("*** Entering MyMoneyStorageSql::writeKeyValuePairs");
+ QMap<QString, QString>::const_iterator it;
+ for(it = pairs.begin(); it != pairs.end(); ++it) {
+ writeKeyValuePair (kvpType, kvpId, it.key(), it.data());
+ }
+}
+
+void MyMoneyStorageSql::writeKeyValuePair (const QString& kvpType, const QString& kvpId, const QString& kvpKey, const QString& kvpData) {
+ DBG("*** Entering MyMoneyStorageSql::writeKeyValuePair");
+ MyMoneySqlQuery q(this);
+ q.prepare (m_db.m_tables["kmmKeyValuePairs"].insertString());
+ q.bindValue(":kvpType", kvpType);
+ q.bindValue(":kvpId", kvpId);
+ q.bindValue(":kvpKey", kvpKey);
+ q.bindValue(":kvpData", kvpData);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("writing KVP")));
+ ++m_kvps;
+}
+
+void MyMoneyStorageSql::deleteKeyValuePairs (const QString& kvpType, const QString& kvpId) {
+ DBG("*** Entering MyMoneyStorageSql::deleteKeyValuePairs");
+ MyMoneySqlQuery q(this);
+ q.prepare ("DELETE FROM kmmKeyValuePairs WHERE kvpType = :kvpType AND kvpId = :kvpId;");
+ q.bindValue(":kvpType", kvpType);
+ q.bindValue(":kvpId", kvpId);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("deleting kvp for %1 %2").arg(kvpType).arg(kvpId)));
+ m_kvps -= q.numRowsAffected();
+}
+
+//******************************** read SQL routines **************************************
+#define CASE(a) if ((*ft)->name() == #a)
+#define GETSTRING q.value(i).toString()
+#define GETCSTRING q.value(i).toCString()
+#define GETDATE getDate(GETSTRING)
+#define GETDATETIME getDateTime(GETSTRING)
+#define GETINT q.value(i).toInt()
+#define GETULL q.value(i).toULongLong()
+
+void MyMoneyStorageSql::readFileInfo(void) {
+ DBG("*** Entering MyMoneyStorageSql::readFileInfo");
+ signalProgress(0, 18, QObject::tr("Loading file information..."));
+ MyMoneyDbTable& t = m_db.m_tables["kmmFileInfo"];
+ MyMoneySqlQuery q(this);
+ q.prepare (t.selectAllString());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading FileInfo")));
+ if (!q.next()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("retrieving FileInfo")));
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ while (ft != t.end()) {
+ // versioning is now handled in open routine
+/* CASE(version) setVersion(GETSTRING); // check version == current version...
+ else*/
+ CASE(created) m_storage->setCreationDate(GETDATE);
+ else CASE(lastModified) m_storage->setLastModificationDate(GETDATE);
+ else CASE(hiInstitutionId) m_hiIdInstitutions = (unsigned long) GETULL;
+ else CASE(hiPayeeId) m_hiIdPayees = (unsigned long) GETULL;
+ else CASE(hiAccountId) m_hiIdAccounts = (unsigned long) GETULL;
+ else CASE(hiTransactionId) m_hiIdTransactions = (unsigned long) GETULL;
+ else CASE(hiScheduleId) m_hiIdSchedules = (unsigned long) GETULL;
+ else CASE(hiSecurityId) m_hiIdSecurities = (unsigned long) GETULL;
+ else CASE(hiReportId ) m_hiIdReports = (unsigned long) GETULL;
+ else CASE(hiBudgetId ) m_hiIdBudgets = (unsigned long) GETULL;
+ else CASE(institutions) m_institutions = (unsigned long) GETULL;
+ else CASE(accounts ) m_accounts = (unsigned long) GETULL;
+ else CASE(payees ) m_payees = (unsigned long) GETULL;
+ else CASE(transactions) m_transactions = (unsigned long) GETULL;
+ else CASE(splits ) m_splits = (unsigned long) GETULL;
+ else CASE(securities ) m_securities = (unsigned long) GETULL;
+ else CASE(currencies ) m_currencies = (unsigned long) GETULL;
+ else CASE(schedules ) m_schedules = (unsigned long) GETULL;
+ else CASE(prices ) m_prices = (unsigned long) GETULL;
+ else CASE(kvps ) m_kvps = (unsigned long) GETULL;
+ else CASE(reports ) m_reports = (unsigned long) GETULL;
+ else CASE(budgets ) m_budgets = (unsigned long) GETULL;
+ else CASE(encryptData) m_encryptData = GETSTRING;
+ else CASE(logonUser) m_logonUser = GETSTRING;
+ else CASE(logonAt) m_logonAt = GETDATETIME;
+ ++ft; ++i;
+ signalProgress(i,0);
+ }
+ m_storage->setPairs(readKeyValuePairs("STORAGE", QString("")).pairs());
+}
+
+/*void MyMoneyStorageSql::setVersion (const QString& version) {
+ DBG("*** Entering MyMoneyStorageSql::setVersion");
+ m_dbVersion = version.section('.', 0, 0).toUInt();
+ m_minorVersion = version.section('.', 1, 1).toUInt();
+ // Okay, I made a cockup by forgetting to include a fixversion in the database
+ // design, so we'll use the minor version as fix level (similar to VERSION
+ // and FIXVERSION in XML file format). A second mistake was setting minor version to 1
+ // in the first place, so we need to subtract one on reading and add one on writing (sigh)!!
+ m_storage->setFileFixVersion( m_minorVersion - 1);
+}*/
+
+void MyMoneyStorageSql::readInstitutions(void) {
+ TRY
+ QMap<QString, MyMoneyInstitution> iList = fetchInstitutions();
+ m_storage->loadInstitutions(iList);
+ readFileInfo();
+ m_storage->loadInstitutionId(m_hiIdInstitutions);
+ PASS
+}
+
+const QMap<QString, MyMoneyInstitution> MyMoneyStorageSql::fetchInstitutions (const QStringList& idList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::readInstitutions");
+ signalProgress(0, m_institutions, QObject::tr("Loading institutions..."));
+ int progress = 0;
+ QMap<QString, MyMoneyInstitution> iList;
+ unsigned long lastId = 0;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmInstitutions"];
+ MyMoneySqlQuery sq(const_cast <MyMoneyStorageSql*> (this));
+ sq.prepare ("SELECT id from kmmAccounts where institutionId = :id");
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString queryString (t.selectAllString(false));
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ if (! idList.empty()) {
+ queryString += " WHERE";
+ for (unsigned i = 0; i < idList.count(); ++i)
+ queryString += " id = :id" + QString::number(i) + " OR";
+ queryString = queryString.left(queryString.length() - 2);
+ }
+ if (forUpdate)
+ queryString += " FOR UPDATE";
+
+ queryString += ";";
+
+ q.prepare (queryString);
+
+ if (! idList.empty()) {
+ QStringList::const_iterator bindVal = idList.begin();
+ for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) {
+ q.bindValue (":id" + QString::number(i), *bindVal);
+ }
+ }
+
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Institution")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString iid;
+ MyMoneyInstitution inst;
+ while (ft != t.end()) {
+ CASE(id) iid = GETSTRING;
+ else CASE(name) inst.setName(GETSTRING);
+ else CASE(manager) inst.setManager(GETSTRING);
+ else CASE(routingCode) inst.setSortcode(GETSTRING);
+ else CASE(addressStreet) inst.setStreet(GETSTRING);
+ else CASE(addressCity) inst.setCity(GETSTRING);
+ else CASE(addressZipcode) inst.setPostcode(GETSTRING);
+ else CASE(telephone) inst.setTelephone(GETSTRING);
+ ++ft; ++i;
+ }
+ // get list of subaccounts
+ sq.bindValue(":id", iid);
+ if (!sq.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Institution AccountList")));
+ QStringList aList;
+ while (sq.next()) aList.append(sq.value(0).toString());
+ for (QStringList::ConstIterator it = aList.begin(); it != aList.end(); ++it)
+ inst.addAccountId(*it);
+
+ iList[iid] = MyMoneyInstitution(iid, inst);
+ unsigned long id = extractId(iid);
+ if(id > lastId)
+ lastId = id;
+
+ signalProgress (++progress, 0);
+ }
+ return iList;
+}
+
+void MyMoneyStorageSql::readPayees (const QString& id) {
+ DBG("*** Entering MyMoneyStorageSql::readPayees");
+ QValueList<QString> list;
+ list.append(id);
+ readPayees(list);
+}
+
+void MyMoneyStorageSql::readPayees(const QValueList<QString> pid) {
+ DBG("*** Entering MyMoneyStorageSql::readPayees");
+ TRY
+ QStringList pidList;
+ qCopy(pid.begin(), pid.end(), qBackInserter(pidList));
+
+ m_storage->loadPayees(fetchPayees(pidList));
+ readFileInfo();
+ m_storage->loadPayeeId(m_hiIdPayees);
+ CATCH
+ delete e; // ignore duplicates
+ ECATCH
+// if (pid.isEmpty()) m_payeeListRead = true;
+}
+
+const QMap<QString, MyMoneyPayee> MyMoneyStorageSql::fetchPayees (const QStringList& idList, bool /*forUpdate*/) const {
+ DBG("*** Entering MyMoneyStorageSql::readPayees");
+ if (m_displayStatus) {
+ signalProgress(0, m_payees, QObject::tr("Loading payees..."));
+ } else {
+// if (m_payeeListRead) return;
+ }
+ int progress = 0;
+ QMap<QString, MyMoneyPayee> pList;
+ //unsigned long lastId;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmPayees"];
+ MyMoneyDbTable::field_iterator payeeEnd = t.end();
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ if (idList.isEmpty()) {
+ q.prepare (t.selectAllString());
+ } else {
+ QString whereClause = " where (";
+ QString itemConnector = "";
+ QStringList::ConstIterator it;
+ for (it = idList.begin(); it != idList.end(); ++it) {
+ whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(*it));
+ itemConnector = " or ";
+ }
+ whereClause += ")";
+ q.prepare (t.selectAllString(false) + whereClause);
+ }
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Payee")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString pid;
+ QString boolChar;
+ MyMoneyPayee payee;
+ unsigned int type;
+ bool ignoreCase;
+ QString matchKeys;
+ while (ft != payeeEnd) {
+ CASE(id) pid = GETCSTRING;
+ else CASE(name) payee.setName(GETSTRING);
+ else CASE(reference) payee.setReference(GETSTRING);
+ else CASE(email) payee.setEmail(GETSTRING);
+ else CASE(addressStreet) payee.setAddress(GETSTRING);
+ else CASE(addressCity) payee.setCity(GETSTRING);
+ else CASE(addressZipcode) payee.setPostcode(GETSTRING);
+ else CASE(addressState) payee.setState(GETSTRING);
+ else CASE(telephone) payee.setTelephone(GETSTRING);
+ else CASE(notes) payee.setNotes(GETSTRING);
+ else CASE(defaultAccountId) payee.setDefaultAccountId(GETSTRING);
+ else CASE(matchData) type = GETINT;
+ else CASE(matchIgnoreCase) ignoreCase = (GETSTRING == "Y");
+ else CASE(matchKeys) matchKeys = GETSTRING;
+ ++ft; ++i;
+ }
+ payee.setMatchData (static_cast<MyMoneyPayee::payeeMatchType>(type), ignoreCase, matchKeys);
+ if (pid == "USER") {
+ TRY
+ m_storage->setUser(payee);
+ PASS
+ } else {
+ pList[pid] = MyMoneyPayee(pid, payee);
+ //unsigned long id = extractId(QString(pid));
+ //if(id > lastId)
+ // lastId = id;
+ }
+ if (m_displayStatus) signalProgress(++progress, 0);
+ }
+ return pList;
+}
+
+const QMap<QString, MyMoneyAccount> MyMoneyStorageSql::fetchAccounts (const QStringList& idList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::fetchAccounts");
+ signalProgress(0, m_accounts, QObject::tr("Loading accounts..."));
+ int progress = 0;
+ QMap<QString, MyMoneyAccount> accList;
+ QStringList kvpAccountList;
+
+ const MyMoneyDbTable& t = m_db.m_tables["kmmAccounts"];
+ MyMoneyDbTable::field_iterator accEnd = t.end();
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ MyMoneySqlQuery sq(const_cast <MyMoneyStorageSql*> (this));
+
+ QString childQueryString = "SELECT id, parentId FROM kmmAccounts WHERE ";
+ QString queryString (t.selectAllString(false));
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ if (! idList.empty()) {
+ kvpAccountList = idList;
+ queryString += " WHERE id IN (";
+ childQueryString += " parentId IN (";
+ for (unsigned i = 0; i < idList.count(); ++i) {
+ queryString += " :id" + QString::number(i) + ", ";
+ childQueryString += ":id" + QString::number(i) + ", ";
+ }
+ queryString = queryString.left(queryString.length() - 2) + ")";
+ childQueryString = childQueryString.left(childQueryString.length() - 2) + ")";
+ } else {
+ childQueryString += " NOT parentId IS NULL";
+ }
+
+ queryString += " ORDER BY id";
+ childQueryString += " ORDER BY parentid, id";
+
+ if (forUpdate) {
+ queryString += " FOR UPDATE";
+ childQueryString += " FOR UPDATE";
+ }
+
+ q.prepare (queryString);
+ sq.prepare (childQueryString);
+
+ if (! idList.empty()) {
+ QStringList::const_iterator bindVal = idList.begin();
+ for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) {
+ q.bindValue (":id" + QString::number(i), *bindVal);
+ sq.bindValue (":id" + QString::number(i), *bindVal);
+ }
+ }
+
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Account")));
+ if (!sq.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading subAccountList")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString aid;
+ QString balance;
+ MyMoneyAccount acc;
+
+ while (ft != accEnd) {
+ CASE(id) aid = GETCSTRING;
+ else CASE(institutionId) acc.setInstitutionId(GETCSTRING);
+ else CASE(parentId) acc.setParentAccountId(GETCSTRING);
+ else CASE(lastReconciled) acc.setLastReconciliationDate(GETDATE);
+ else CASE(lastModified) acc.setLastModified(GETDATE);
+ else CASE(openingDate) acc.setOpeningDate(GETDATE);
+ else CASE(accountNumber) acc.setNumber(GETSTRING);
+ else CASE(accountType) acc.setAccountType(static_cast<MyMoneyAccount::accountTypeE>(GETINT));
+ else CASE(accountName) acc.setName(GETSTRING);
+ else CASE(description) acc.setDescription(GETSTRING);
+ else CASE(currencyId) acc.setCurrencyId(GETCSTRING);
+ else CASE(balance) acc.setBalance(GETSTRING);
+ else CASE(transactionCount)
+ const_cast <MyMoneyStorageSql*> (this)->m_transactionCountMap[aid] = (unsigned long) GETULL;
+ ++ft; ++i;
+ }
+
+ // Process any key value pair
+ if (idList.empty())
+ kvpAccountList.append(aid);
+
+ // in database mode, load the balance from the account record
+ // else we would need to read all the transactions
+ accList.insert(aid, MyMoneyAccount(aid, acc));
+ if (acc.value("PreferredAccount") == "Yes") {
+ const_cast <MyMoneyStorageSql*> (this)->m_preferred.addAccount(aid);
+ }
+ signalProgress(++progress, 0);
+ }
+
+ QMapIterator<QString, MyMoneyAccount> it_acc;
+ QMapIterator<QString, MyMoneyAccount> accListEnd = accList.end();
+ while (sq.next()) {
+ it_acc = accList.find(sq.value(1).toString());
+ if (it_acc != accListEnd && it_acc.data().id() == sq.value(1).toString()) {
+ while (sq.isValid() && it_acc != accListEnd
+ && it_acc.data().id() == sq.value(1).toString()) {
+ it_acc.data().addAccountId(sq.value(0).toString());
+ sq.next();
+ }
+ sq.prev();
+ }
+ }
+
+ //TODO: There should be a better way than this. What's below is O(n log n) or more,
+ // where it may be able to be done in O(n), if things are just right.
+ // The operator[] call in the loop is the most expensive call in this function, according
+ // to several profile runs.
+ QMap <QString, MyMoneyKeyValueContainer> kvpResult = readKeyValuePairs("ACCOUNT", kvpAccountList);
+ QMap <QString, MyMoneyKeyValueContainer>::const_iterator kvp_end = kvpResult.end();
+ for (QMap <QString, MyMoneyKeyValueContainer>::const_iterator it_kvp = kvpResult.begin();
+ it_kvp != kvp_end; ++it_kvp) {
+ accList[it_kvp.key()].setPairs(it_kvp.data().pairs());
+ }
+
+ kvpResult = readKeyValuePairs("ONLINEBANKING", kvpAccountList);
+ kvp_end = kvpResult.end();
+ for (QMap <QString, MyMoneyKeyValueContainer>::const_iterator it_kvp = kvpResult.begin();
+ it_kvp != kvp_end; ++it_kvp) {
+ accList[it_kvp.key()].setOnlineBankingSettings(it_kvp.data());
+ }
+
+ return accList;
+}
+
+void MyMoneyStorageSql::readAccounts(void) {
+ m_storage->loadAccounts(fetchAccounts());
+ m_storage->loadAccountId(m_hiIdAccounts);
+}
+
+const QMap<QString, MyMoneyMoney> MyMoneyStorageSql::fetchBalance(const QStringList& idList, const QDate& date) const {
+
+ QMap<QString, MyMoneyMoney> returnValue;
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString queryString = "SELECT action, shares, accountId, postDate "
+ "FROM kmmSplits WHERE txType = 'N' AND accountId in (";
+
+ for (unsigned i = 0; i < idList.count(); ++i) {
+ queryString += " :id" + QString::number(i) + ", ";
+ }
+ queryString = queryString.left(queryString.length() - 2) + " )";
+
+ // SQLite stores dates as YYYY-MM-DDTHH:mm:ss with 0s for the time part. This makes
+ // the <= operator misbehave when the date matches. To avoid this, add a day to the
+ // requested date and use the < operator.
+ if (date.isValid() && !date.isNull())
+ queryString += QString(" AND postDate < '%1'").arg(date.addDays(1).toString(Qt::ISODate));
+ DBG (queryString);
+ q.prepare(queryString);
+
+ QStringList::const_iterator bindVal = idList.begin();
+ for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) {
+ q.bindValue (":id" + QString::number(i), *bindVal);
+ returnValue[*bindVal] = MyMoneyMoney(0);
+ }
+ if (!q.exec())
+ throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("fetching balance")));
+ QString id;
+ QString shares;
+ QString action;
+ while (q.next()) {
+ id = q.value(2).toString();
+ shares = q.value(1).toString();
+ action = q.value(0).toString();
+ if (MyMoneySplit::ActionSplitShares == action)
+ returnValue[id] = returnValue[id] * MyMoneyMoney(shares);
+ else
+ returnValue[id] += MyMoneyMoney(shares);
+ }
+ return returnValue;
+}
+
+void MyMoneyStorageSql::readTransactions(const QString& tidList, const QString& dateClause) {
+ TRY
+ m_storage->loadTransactions(fetchTransactions(tidList, dateClause));
+ m_storage->loadTransactionId(m_hiIdTransactions);
+ PASS
+}
+
+void MyMoneyStorageSql::readTransactions(const MyMoneyTransactionFilter& filter) {
+ TRY
+ m_storage->loadTransactions(fetchTransactions(filter));
+ m_storage->loadTransactionId(m_hiIdTransactions);
+ PASS
+}
+
+const QMap<QString, MyMoneyTransaction> MyMoneyStorageSql::fetchTransactions (const QString& tidList, const QString& dateClause, bool /*forUpdate*/) const {
+ DBG("*** Entering MyMoneyStorageSql::readTransactions");
+// if (m_transactionListRead) return; // all list already in memory
+ if (m_displayStatus) signalProgress(0, m_transactions, QObject::tr("Loading transactions..."));
+ int progress = 0;
+// m_payeeList.clear();
+ QString whereClause;
+ whereClause = " WHERE txType = 'N' ";
+ if (! tidList.isEmpty()) {
+ whereClause += " AND id IN " + tidList;
+ }
+ if (!dateClause.isEmpty()) whereClause += " and " + dateClause;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmTransactions"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare (t.selectAllString(false) + whereClause + " ORDER BY id;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Transaction")));
+ const MyMoneyDbTable& ts = m_db.m_tables["kmmSplits"];
+ whereClause = " WHERE txType = 'N' ";
+ if (! tidList.isEmpty()) {
+ whereClause += " AND transactionId IN " + tidList;
+ }
+ if (!dateClause.isEmpty()) whereClause += " and " + dateClause;
+ MyMoneySqlQuery qs(const_cast <MyMoneyStorageSql*> (this));
+ QString splitQuery = ts.selectAllString(false) + whereClause
+ + " ORDER BY transactionId, splitId;";
+ qs.prepare (splitQuery);
+ if (!qs.exec()) throw new MYMONEYEXCEPTION(buildError (qs, __func__, "reading Splits"));
+ QString splitTxId = "ZZZ";
+ MyMoneySplit s;
+ if (qs.next()) {
+ splitTxId = qs.value(0).toString();
+ readSplit (s, qs, ts);
+ } else {
+ splitTxId = "ZZZ";
+ }
+ QMap <QString, MyMoneyTransaction> txMap;
+ QStringList txList;
+ MyMoneyDbTable::field_iterator txEnd = t.end();
+ while (q.next()) {
+ MyMoneyTransaction tx;
+ QString txId;
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ while (ft != txEnd) {
+ CASE(id) txId = GETSTRING;
+ else CASE(postDate) tx.setPostDate(GETDATE);
+ else CASE(memo) tx.setMemo(GETSTRING);
+ else CASE(entryDate) tx.setEntryDate(GETDATE);
+ else CASE(currencyId) tx.setCommodity(GETCSTRING);
+ else CASE(bankId) tx.setBankID(GETSTRING);
+ ++ft; ++i;
+ }
+
+ while (txId < splitTxId && splitTxId != "ZZZ") {
+ if (qs.next()) {
+ splitTxId = qs.value(0).toString();
+ readSplit (s, qs, ts);
+ } else {
+ splitTxId = "ZZZ";
+ }
+ }
+
+ while (txId == splitTxId) {
+ tx.addSplit (s);
+ if (qs.next()) {
+ splitTxId = qs.value(0).toString();
+ readSplit (s, qs, ts);
+ } else {
+ splitTxId = "ZZZ";
+ }
+ }
+ // Process any key value pair
+ if (! txId.isEmpty()) {
+ txList.append(txId);
+ tx = MyMoneyTransaction(txId, tx);
+ txMap.insert(tx.uniqueSortKey(), tx);
+ }
+ }
+ QMap <QString, MyMoneyKeyValueContainer> kvpMap = readKeyValuePairs("TRANSACTION", txList);
+ QMap<QString, MyMoneyTransaction> tList;
+ QMapIterator<QString, MyMoneyTransaction> txMapEnd = txMap.end();
+ for (QMapIterator<QString, MyMoneyTransaction> i = txMap.begin();
+ i != txMapEnd; ++i) {
+ i.data().setPairs(kvpMap[i.data().id()].pairs());
+
+ if (m_displayStatus) signalProgress(++progress, 0);
+ }
+
+ if ((tidList.isEmpty()) && (dateClause.isEmpty())) {
+ //qDebug("setting full list read");
+ }
+ return txMap;
+}
+
+int MyMoneyStorageSql::splitState(const MyMoneyTransactionFilter::stateOptionE& state) const
+{
+ int rc = MyMoneySplit::NotReconciled;
+
+ switch(state) {
+ default:
+ case MyMoneyTransactionFilter::notReconciled:
+ break;
+
+ case MyMoneyTransactionFilter::cleared:
+ rc = MyMoneySplit::Cleared;
+ break;
+
+ case MyMoneyTransactionFilter::reconciled:
+ rc = MyMoneySplit::Reconciled;
+ break;
+
+ case MyMoneyTransactionFilter::frozen:
+ rc = MyMoneySplit::Frozen;
+ break;
+ }
+ return rc;
+}
+
+const QMap<QString, MyMoneyTransaction> MyMoneyStorageSql::fetchTransactions (const MyMoneyTransactionFilter& filter) const {
+ DBG("*** Entering MyMoneyStorageSql::readTransactions");
+ // analyze the filter
+// if (m_transactionListRead) return; // all list already in memory
+ // if the filter is restricted to certain accounts/categories
+ // check if we already have them all in memory
+ QStringList accounts;
+ QString inQuery;
+ filter.accounts(accounts);
+ filter.categories(accounts);
+// QStringList::iterator it;
+// bool allAccountsLoaded = true;
+// for (it = accounts.begin(); it != accounts.end(); ++it) {
+// if (m_accountsLoaded.find(*it) == m_accountsLoaded.end()) {
+// allAccountsLoaded = false;
+// break;
+// }
+// }
+// if (allAccountsLoaded) return;
+ /* Some filter combinations do not lend themselves to implementation
+ * in SQL, or are likely to require such extensive reading of the database
+ * as to make it easier to just read everything into memory. */
+ bool canImplementFilter = true;
+ MyMoneyMoney m1, m2;
+ if (filter.amountFilter( m1, m2 )) {
+ alert ("Amount Filter Set");
+ canImplementFilter = false;
+ }
+ QString n1, n2;
+ if (filter.numberFilter(n1, n2)) {
+ alert("Number filter set");
+ canImplementFilter = false;
+ }
+ int t1;
+ if (filter.firstType(t1)) {
+ alert("Type filter set");
+ canImplementFilter = false;
+ }
+// int s1;
+// if (filter.firstState(s1)) {
+// alert("State filter set");
+// canImplementFilter = false;
+// }
+ QRegExp t2;
+ if (filter.textFilter(t2)) {
+ alert("text filter set");
+ canImplementFilter = false;
+ }
+ MyMoneyTransactionFilter::FilterSet s = filter.filterSet();
+ if (s.singleFilter.validityFilter) {
+ alert("Validity filter set");
+ canImplementFilter = false;
+ }
+ if (!canImplementFilter) {
+ QMap<QString, MyMoneyTransaction> transactionList = fetchTransactions();
+ QMap<QString, MyMoneyTransaction>::ConstIterator it_t;
+ QMap<QString, MyMoneyTransaction>::ConstIterator txListEnd = transactionList.end();
+
+ std::remove_if(transactionList.begin(), transactionList.end(), FilterFail(filter, m_storagePtr));
+ return transactionList;
+ }
+
+ bool accountsOnlyFilter = true;
+ bool splitFilterActive = false; // the split filter is active if we are selecting on fields in the split table
+ // get start and end dates
+ QDate start = filter.fromDate();
+ QDate end = filter.toDate();
+ // not entirely sure if the following is correct, but at best, saves a lot of reads, at worst
+ // it only causes us to read a few more transactions that strictly necessary (I think...)
+ if (start == KMyMoneySettings::startDate().date()) start = QDate();
+ bool txFilterActive = ((start != QDate()) || (end != QDate())); // and this for fields in the transaction table
+ if (txFilterActive) accountsOnlyFilter = false;
+
+ QString whereClause = "";
+ QString subClauseconnector = " where txType = 'N' and ";
+ // payees
+ QStringList payees;
+ //filter.payees(payees);
+ if (filter.payees(payees)) {
+ accountsOnlyFilter = false;
+ QString itemConnector = "payeeId in (";
+ QString payeesClause = "";
+ QStringList::const_iterator it;
+ for (it = payees.begin(); it != payees.end(); ++it) {
+ payeesClause.append(QString("%1'%2'")
+ .arg(itemConnector).arg(*it));
+ itemConnector = ", ";
+ }
+ if (!payeesClause.isEmpty()) {
+ whereClause += subClauseconnector + payeesClause + ")";
+ subClauseconnector = " and ";
+ }
+ splitFilterActive = true;
+ }
+
+ // accounts and categories
+ if (!accounts.isEmpty()) {
+ splitFilterActive = true;
+ QString itemConnector = "accountId in (";
+ QString accountsClause = "";
+ QStringList::const_iterator it;
+ for (it = accounts.begin(); it != accounts.end(); ++it) {
+// if (m_accountsLoaded.find(*it) == m_accountsLoaded.end()) {
+ accountsClause.append(QString("%1 '%2'")
+ .arg(itemConnector).arg(*it));
+ itemConnector = ", ";
+ //if (accountsOnlyFilter) m_accountsLoaded.append(*it); // a bit premature...
+// }
+ }
+ if (!accountsClause.isEmpty()) {
+ whereClause += subClauseconnector + accountsClause + ")";
+ subClauseconnector = " and (";
+ }
+ }
+
+ // split states
+ QValueList <int> splitStates;
+ if (filter.states(splitStates)) {
+ splitFilterActive = true;
+ QString itemConnector = " reconcileFlag IN (";
+ QString statesClause = "";
+ for (QValueList<int>::ConstIterator it = splitStates.begin(); it != splitStates.end(); ++it) {
+ statesClause.append(QString(" %1 '%2'")
+ .arg(itemConnector)
+ .arg(splitState(MyMoneyTransactionFilter::stateOptionE(*it))));
+ itemConnector = ",";
+ }
+ if (!statesClause.isEmpty()) {
+ whereClause += subClauseconnector + statesClause + ")";
+ subClauseconnector = " and (";
+ }
+ }
+ // I've given up trying to work out the logic. we keep getting the wrong number of close brackets
+ int obc = whereClause.contains('(');
+ int cbc = whereClause.contains(')');
+ if (cbc > obc) {
+ qFatal("invalid where clause - %s", whereClause.latin1());
+ }
+ while (cbc < obc) {
+ whereClause.append(")");
+ cbc++;
+ }
+ // if the split filter is active, but the where clause is empty
+ // it means we already have all the transactions for the specified filter
+ // in memory, so just exit
+ if ((splitFilterActive) && (whereClause.isEmpty())) {
+ qDebug("all transactions already in storage");
+ return fetchTransactions();
+ }
+
+ // if we have neither a split filter, nor a tx (date) filter
+ // it's effectively a read all
+ if ((!splitFilterActive) && (!txFilterActive)) {
+ //qDebug("reading all transactions");
+ return fetchTransactions();
+ }
+ // build a date clause for the transaction table
+ QString dateClause;
+ QString connector = "";
+ if (end != QDate()) {
+ dateClause = QString("(postDate < '%1')").arg(end.addDays(1).toString(Qt::ISODate));
+ connector = " and ";
+ }
+ if (start != QDate()) {
+ dateClause += QString("%1 (postDate >= '%2')").arg(connector).arg(start.toString(Qt::ISODate));
+ }
+ // now get a list of transaction ids
+ // if we have only a date filter, we need to build the list from the tx table
+ // otherwise we need to build from the split table
+ if (splitFilterActive) {
+ inQuery = QString("(select distinct transactionId from kmmSplits %1)").arg(whereClause);
+ } else {
+ inQuery = QString("(select distinct id from kmmTransactions where %1)").arg(dateClause);
+ txFilterActive = false; // kill off the date filter now
+ }
+
+ return fetchTransactions(inQuery, dateClause);
+ //FIXME: if we have an accounts-only filter, recalc balances on loaded accounts
+}
+
+unsigned long MyMoneyStorageSql::transactionCount (const QString& aid) const {
+ DBG("*** Entering MyMoneyStorageSql::transactionCount");
+ if (aid.length() == 0)
+ return m_transactions;
+ else
+ return m_transactionCountMap[aid];
+}
+
+void MyMoneyStorageSql::readSplit (MyMoneySplit& s, const MyMoneySqlQuery& q, const MyMoneyDbTable& t) const {
+ DBG("*** Entering MyMoneyStorageSql::readSplit");
+ s.clearId();
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ MyMoneyDbTable::field_iterator splitEnd = t.end();
+ int i = 0;
+
+ // Use the QString here instead of CASE, since this is called so often.
+ QString fieldName;
+ while (ft != splitEnd) {
+ fieldName = (*ft)->name();
+ if (fieldName == "payeeId") s.setPayeeId(GETCSTRING);
+ else if (fieldName == "reconcileDate") s.setReconcileDate(GETDATE);
+ else if (fieldName == "action") s.setAction(GETCSTRING);
+ else if (fieldName == "reconcileFlag") s.setReconcileFlag(static_cast<MyMoneySplit::reconcileFlagE>(GETINT));
+ else if (fieldName == "value") s.setValue(MyMoneyMoney(QStringEmpty(GETSTRING)));
+ else if (fieldName == "shares") s.setShares(MyMoneyMoney(QStringEmpty(GETSTRING)));
+ else if (fieldName == "price") s.setPrice(MyMoneyMoney(QStringEmpty(GETSTRING)));
+ else if (fieldName == "memo") s.setMemo(GETSTRING);
+ else if (fieldName == "accountId") s.setAccountId(GETCSTRING);
+ else if (fieldName == "checkNumber") s.setNumber(GETSTRING);
+ //else if (fieldName == "postDate") s.setPostDate(GETDATETIME); // FIXME - when Tom puts date into split object
+ else if (fieldName == "bankId") s.setBankID(GETSTRING);
+ ++ft; ++i;
+ }
+
+ return;
+}
+
+bool MyMoneyStorageSql::isReferencedByTransaction(const QString& id) const {
+ DBG("*** Entering MyMoneyStorageSql::isReferencedByTransaction");
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare("SELECT COUNT(*) FROM kmmTransactions "
+ "INNER JOIN kmmSplits ON kmmTransactions.id = kmmSplits.transactionId "
+ "WHERE kmmTransactions.currencyId = :ID OR kmmSplits.payeeId = :ID "
+ "OR kmmSplits.accountId = :ID");
+ q.bindValue(":ID", id);
+ if ((!q.exec()) || (!q.next())) {
+ buildError (q, __func__, "error retrieving reference count");
+ qFatal("Error retrieving reference count"); // definitely shouldn't happen
+ }
+ return (0 != q.value(0).toULongLong());
+}
+
+void MyMoneyStorageSql::readSchedules(void) {
+
+ TRY
+ m_storage->loadSchedules(fetchSchedules());
+ readFileInfo();
+ m_storage->loadScheduleId(m_hiIdSchedules);
+ PASS
+}
+
+const QMap<QString, MyMoneySchedule> MyMoneyStorageSql::fetchSchedules (const QStringList& idList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::readSchedules");
+ signalProgress(0, m_schedules, QObject::tr("Loading schedules..."));
+ int progress = 0;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmSchedules"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QMap<QString, MyMoneySchedule> sList;
+ //unsigned long lastId = 0;
+ const MyMoneyDbTable& ts = m_db.m_tables["kmmSplits"];
+ MyMoneySqlQuery qs(const_cast <MyMoneyStorageSql*> (this));
+ qs.prepare (ts.selectAllString(false) + " WHERE transactionId = :id ORDER BY splitId;");
+ MyMoneySqlQuery sq(const_cast <MyMoneyStorageSql*> (this));
+ sq.prepare ("SELECT payDate from kmmSchedulePaymentHistory where schedId = :id");
+
+ QString queryString (t.selectAllString(false));
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ if (! idList.empty()) {
+ queryString += " WHERE";
+ for (unsigned i = 0; i < idList.count(); ++i)
+ queryString += " id = :id" + QString::number(i) + " OR";
+ queryString = queryString.left(queryString.length() - 2);
+ }
+ queryString += " ORDER BY id;";
+
+ if (forUpdate)
+ queryString += " FOR UPDATE";
+
+ queryString += ";";
+
+ q.prepare (queryString);
+
+ if (! idList.empty()) {
+ QStringList::const_iterator bindVal = idList.begin();
+ for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) {
+ q.bindValue (":id" + QString::number(i), *bindVal);
+ }
+ }
+
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Schedules")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ MyMoneySchedule s;
+ QString sId;
+ QString boolChar;
+ QDate nextPaymentDue;
+ while (ft != t.end()) {
+ CASE(id) sId = GETCSTRING;
+ else CASE(name) s.setName (GETSTRING);
+ else CASE(type) s.setType (static_cast<MyMoneySchedule::typeE>(GETINT));
+ else CASE(occurence) s.setOccurencePeriod (static_cast<MyMoneySchedule::occurenceE>(GETINT));
+ else CASE(occurenceMultiplier) s.setOccurenceMultiplier (GETINT);
+ else CASE(paymentType) s.setPaymentType (static_cast<MyMoneySchedule::paymentTypeE>(GETINT));
+ else CASE(startDate) s.setStartDate (GETDATE);
+ else CASE(endDate) s.setEndDate (GETDATE);
+ else CASE(fixed) {boolChar = GETSTRING; s.setFixed (boolChar == "Y");}
+ else CASE(autoEnter) {boolChar = GETSTRING; s.setAutoEnter (boolChar == "Y");}
+ else CASE(lastPayment) s.setLastPayment (GETDATE);
+ else CASE(weekendOption)
+ s.setWeekendOption (static_cast<MyMoneySchedule::weekendOptionE>(GETINT));
+ else CASE(nextPaymentDue) nextPaymentDue = GETDATE;
+ ++ft; ++i;
+ }
+ // convert simple occurence to compound occurence
+ int mult = s.occurenceMultiplier();
+ MyMoneySchedule::occurenceE occ = s.occurencePeriod();
+ MyMoneySchedule::simpleToCompoundOccurence(mult,occ);
+ s.setOccurencePeriod(occ);
+ s.setOccurenceMultiplier(mult);
+ // now assign the id to the schedule
+ MyMoneySchedule _s(sId, s);
+ s = _s;
+ // read the associated transaction
+// m_payeeList.clear();
+ const MyMoneyDbTable& t = m_db.m_tables["kmmTransactions"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare (t.selectAllString(false) + " WHERE id = :id;");
+ q.bindValue(":id", s.id());
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Scheduled Transaction")));
+ if (!q.next()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("retrieving scheduled transaction")));
+ MyMoneyTransaction tx(s.id(), MyMoneyTransaction());
+ ft = t.begin();
+ i = 0;
+ while (ft != t.end()) {
+ CASE(postDate) tx.setPostDate(GETDATE);
+ else CASE(memo) tx.setMemo(GETSTRING);
+ else CASE(entryDate) tx.setEntryDate(GETDATE);
+ else CASE(currencyId) tx.setCommodity(GETCSTRING);
+ else CASE(bankId) tx.setBankID(GETSTRING);
+ ++ft; ++i;
+ }
+
+ qs.bindValue(":id", s.id());
+ if (!qs.exec()) throw new MYMONEYEXCEPTION(buildError (qs, __func__, "reading Scheduled Splits"));
+ while (qs.next()) {
+ MyMoneySplit sp;
+ readSplit (sp, qs, ts);
+ tx.addSplit (sp);
+ }
+// if (!m_payeeList.isEmpty())
+// readPayees(m_payeeList);
+ // Process any key value pair
+ tx.setPairs(readKeyValuePairs("TRANSACTION", s.id()).pairs());
+
+ // If the transaction doesn't have a post date, setTransaction will reject it.
+ // The old way of handling things was to store the next post date in the schedule object
+ // and set the transaction post date to QDate().
+ // For compatibility, if this is the case, copy the next post date from the schedule object
+ // to the transaction object post date.
+ if (!tx.postDate().isValid()) {
+ tx.setPostDate(nextPaymentDue);
+ }
+
+ s.setTransaction(tx);
+
+ // read in the recorded payments
+ sq.bindValue(":id", s.id());
+ if (!sq.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading schedule payment history")));
+ while (sq.next()) s.recordPayment (sq.value(0).toDate());
+
+ sList[s.id()] = s;
+
+ //FIXME: enable when schedules have KVPs.
+ // s.setPairs(readKeyValuePairs("SCHEDULE", s.id()).pairs());
+
+ //unsigned long id = extractId(s.id().data());
+ //if(id > lastId)
+ // lastId = id;
+
+ signalProgress(++progress, 0);
+ }
+ return sList;
+}
+
+void MyMoneyStorageSql::readSecurities(void) {
+ TRY
+ m_storage->loadSecurities(fetchSecurities());
+ readFileInfo();
+ m_storage->loadSecurityId(m_hiIdSecurities);
+ PASS
+}
+
+const QMap<QString, MyMoneySecurity> MyMoneyStorageSql::fetchSecurities (const QStringList& /*idList*/, bool /*forUpdate*/) const {
+ DBG("*** Entering MyMoneyStorageSql::readSecurities");
+ signalProgress(0, m_securities, QObject::tr("Loading securities..."));
+ int progress = 0;
+ QMap<QString, MyMoneySecurity> sList;
+ unsigned long lastId = 0;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmSecurities"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare (t.selectAllString(false) + " ORDER BY id;");
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Securities")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ MyMoneySecurity e;
+ QString eid;
+ int saf = 0;
+ while (ft != t.end()) {
+ CASE(id) eid = GETSTRING;
+ else CASE(name) e.setName(GETSTRING);
+ else CASE(symbol) e.setTradingSymbol(GETSTRING);
+ else CASE(type) e.setSecurityType(static_cast<MyMoneySecurity::eSECURITYTYPE>(GETINT));
+ else CASE(smallestAccountFraction) saf = GETINT;
+ else CASE(tradingCurrency) e.setTradingCurrency(GETCSTRING);
+ else CASE(tradingMarket) e.setTradingMarket(GETSTRING);
+ ++ft; ++i;
+ }
+ if(e.tradingCurrency().isEmpty())
+ e.setTradingCurrency(m_storage->pairs()["kmm-baseCurrency"]);
+ if(saf == 0)
+ saf = 100;
+ e.setSmallestAccountFraction(saf);
+
+ // Process any key value pairs
+ e.setPairs(readKeyValuePairs("SECURITY", eid).pairs());
+ //tell the storage objects we have a new security object.
+
+ // FIXME: Adapt to new interface make sure, to take care of the currencies as well
+ // see MyMoneyStorageXML::readSecurites()
+ MyMoneySecurity security(eid,e);
+ sList[security.id()] = security;
+
+ unsigned long id = extractId(security.id());
+ if(id > lastId)
+ lastId = id;
+
+ signalProgress(++progress, 0);
+ }
+ return sList;
+}
+
+void MyMoneyStorageSql::readPrices(void) {
+
+ TRY
+// m_storage->addPrice(MyMoneyPrice(from, to, date, rate, source));
+ PASS
+
+}
+
+const MyMoneyPrice MyMoneyStorageSql::fetchSinglePrice (const QString& fromIdList, const QString& toIdList, const QDate& date_, bool exactDate, bool /*forUpdate*/) const {
+ DBG("*** Entering MyMoneyStorageSql::fetchSinglePrice");
+ const MyMoneyDbTable& t = m_db.m_tables["kmmPrices"];
+ MyMoneyDbTable::field_iterator tableEnd = t.end();
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString queryString = t.selectAllString(false);
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ // See balance query for why the date logic seems odd.
+ queryString += " WHERE fromId = :fromId AND toId = :toId AND priceDate < :priceDate ";
+ if (exactDate)
+ queryString += "AND priceDate > :exactDate ";
+
+ queryString += "ORDER BY priceDate DESC;";
+
+ q.prepare(queryString);
+
+ QDate date (date_);
+
+ if(!date.isValid())
+ date = QDate::currentDate();
+
+ q.bindValue(":fromId", fromIdList);
+ q.bindValue(":toId", toIdList);
+ q.bindValue(":priceDate", date.addDays(1).toString(Qt::ISODate));
+
+ if (exactDate)
+ q.bindValue(":exactDate", date.toString(Qt::ISODate));
+
+ if (! q.exec()) {}
+
+ if (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString from;
+ QString to;
+ QDate date;
+ MyMoneyMoney rate;
+ QString source;
+ bool foundFromId = false;
+ bool foundToId = false;
+ bool foundPriceDate = false;
+ bool foundPrice = false;
+ bool foundPriceSource = false;
+ while (ft != tableEnd) {
+ bool foundSomething = false;
+ if (!foundFromId && !foundSomething) {
+ CASE(fromId) {from = GETCSTRING; foundFromId = true; foundSomething = true;}
+ }
+ if (!foundToId && !foundSomething) {
+ CASE(toId) {to = GETCSTRING; foundToId = true; foundSomething = true;}
+ }
+ if (!foundPriceDate && !foundSomething) {
+ CASE(priceDate) {date = GETDATE; foundPriceDate = true; foundSomething = true;}
+ }
+ if (!foundPrice && !foundSomething) {
+ CASE(price) {rate = GETSTRING; foundPrice = true; foundSomething = true;}
+ }
+ if (!foundPriceSource && !foundSomething) {
+ CASE(priceSource) {source = GETSTRING; foundPriceSource = true; foundSomething = true;}
+ }
+ ++ft; ++i;
+ }
+
+ return MyMoneyPrice(fromIdList, toIdList, date, rate, source);
+ }
+
+ return MyMoneyPrice();
+}
+
+const MyMoneyPriceList MyMoneyStorageSql::fetchPrices (const QStringList& fromIdList, const QStringList& toIdList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::readPrices");
+ signalProgress(0, m_prices, QObject::tr("Loading prices..."));
+ int progress = 0;
+ const_cast <MyMoneyStorageSql*> (this)->m_readingPrices = true;
+ MyMoneyPriceList pList;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmPrices"];
+ MyMoneyDbTable::field_iterator tableEnd = t.end();
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString queryString = t.selectAllString(false);
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ if (! fromIdList.empty()) {
+ queryString += " WHERE (";
+ for (unsigned i = 0; i < fromIdList.count(); ++i) {
+ queryString += " fromId = :fromId" + QString::number(i) + " OR";
+ }
+ queryString = queryString.left(queryString.length() - 2) + ")";
+ }
+ if (! toIdList.empty()) {
+ queryString += " AND (";
+ for (unsigned i = 0; i < toIdList.count(); ++i) {
+ queryString += " toId = :toId" + QString::number(i) + " OR";
+ }
+ queryString = queryString.left(queryString.length() - 2) + ")";
+ }
+
+
+ if (forUpdate)
+ queryString += " FOR UPDATE";
+
+ queryString += ";";
+
+ q.prepare (queryString);
+
+ if (! fromIdList.empty()) {
+ QStringList::const_iterator bindVal = fromIdList.begin();
+ for (int i = 0; bindVal != fromIdList.end(); ++i, ++bindVal) {
+ q.bindValue (":fromId" + QString::number(i), *bindVal);
+ }
+ }
+ if (! toIdList.empty()) {
+ QStringList::const_iterator bindVal = toIdList.begin();
+ for (int i = 0; bindVal != toIdList.end(); ++i, ++bindVal) {
+ q.bindValue (":toId" + QString::number(i), *bindVal);
+ }
+ }
+
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Prices")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString from;
+ QString to;
+ QDate date;
+ MyMoneyMoney rate;
+ QString source;
+
+ while (ft != tableEnd) {
+ CASE(fromId) from = GETCSTRING;
+ else CASE(toId) to = GETCSTRING;
+ else CASE(priceDate) date = GETDATE;
+ else CASE(price) rate = GETSTRING;
+ else CASE(priceSource) source = GETSTRING;
+ ++ft; ++i;
+ }
+ pList [MyMoneySecurityPair(from, to)].insert(date, MyMoneyPrice(from, to, date, rate, source));
+ signalProgress(++progress, 0);
+ }
+ const_cast <MyMoneyStorageSql*> (this)->m_readingPrices = false;
+
+ return pList;
+}
+
+void MyMoneyStorageSql::readCurrencies(void) {
+ TRY
+ m_storage->loadCurrencies(fetchCurrencies());
+ PASS
+}
+
+const QMap<QString, MyMoneySecurity> MyMoneyStorageSql::fetchCurrencies (const QStringList& idList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::readCurrencies");
+ signalProgress(0, m_currencies, QObject::tr("Loading currencies..."));
+ int progress = 0;
+ QMap<QString, MyMoneySecurity> cList;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmCurrencies"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+
+ QString queryString (t.selectAllString(false));
+
+ // Use bind variables, instead of just inserting the values in the queryString,
+ // so that values containing a ':' will work.
+ if (! idList.empty()) {
+ queryString += " WHERE";
+ for (unsigned i = 0; i < idList.count(); ++i)
+ queryString += " isocode = :id" + QString::number(i) + " OR";
+ queryString = queryString.left(queryString.length() - 2);
+ }
+
+ queryString += " ORDER BY ISOcode";
+
+ if (forUpdate)
+ queryString += " FOR UPDATE";
+
+ queryString += ";";
+
+ q.prepare (queryString);
+
+ if (! idList.empty()) {
+ QStringList::const_iterator bindVal = idList.begin();
+ for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) {
+ q.bindValue (":id" + QString::number(i), *bindVal);
+ }
+ }
+
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Currencies")));
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QString id;
+ MyMoneySecurity c;
+ QChar symbol[3];
+ while (ft != t.end()) {
+ CASE(ISOcode) id = GETCSTRING;
+ else CASE(name) c.setName(GETSTRING);
+ else CASE(type) c.setSecurityType(static_cast<MyMoneySecurity::eSECURITYTYPE>(GETINT));
+ else CASE(symbol1) symbol[0] = QChar(GETINT);
+ else CASE(symbol2) symbol[1] = QChar(GETINT);
+ else CASE(symbol3) symbol[2] = QChar(GETINT);
+ else CASE(partsPerUnit) c.setPartsPerUnit(GETINT);
+ else CASE(smallestCashFraction) c.setSmallestCashFraction(GETINT);
+ else CASE(smallestAccountFraction) c.setSmallestAccountFraction(GETINT);
+ ++ft; ++i;
+ }
+ c.setTradingSymbol(QString(symbol, 3).stripWhiteSpace());
+
+ cList[id] = MyMoneySecurity(id, c);
+
+ signalProgress(++progress, 0);
+ }
+ return cList;
+}
+
+void MyMoneyStorageSql::readReports(void) {
+ TRY
+ m_storage->loadReports(fetchReports());
+ readFileInfo();
+ m_storage->loadReportId(m_hiIdReports);
+ PASS
+}
+
+const QMap<QString, MyMoneyReport> MyMoneyStorageSql::fetchReports (const QStringList& /*idList*/, bool /*forUpdate*/) const {
+ DBG("*** Entering MyMoneyStorageSql::readReports");
+ signalProgress(0, m_reports, QObject::tr("Loading reports..."));
+ int progress = 0;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmReportConfig"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare (t.selectAllString(true));
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading reports")));
+ QMap<QString, MyMoneyReport> rList;
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QDomDocument d;
+ while (ft != t.end()) {
+ CASE(XML) d.setContent(GETSTRING, false);
+ ++ft; ++i;
+ }
+ QDomNode child = d.firstChild();
+ child = child.firstChild();
+ MyMoneyReport report;
+
+ if (report.read(child.toElement()))
+ rList[report.id()] = report;
+
+ signalProgress(++progress, 0);
+ }
+ return rList;
+}
+
+const QMap<QString, MyMoneyBudget> MyMoneyStorageSql::fetchBudgets (const QStringList& idList, bool forUpdate) const {
+ DBG("*** Entering MyMoneyStorageSql::readBudgets");
+ signalProgress(0, m_budgets, QObject::tr("Loading budgets..."));
+ int progress = 0;
+ const MyMoneyDbTable& t = m_db.m_tables["kmmBudgetConfig"];
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString queryString (t.selectAllString(false));
+ if (! idList.empty()) {
+ queryString += " WHERE id = '" + idList.join("' OR id = '") + "'";
+ }
+ if (forUpdate)
+ queryString += " FOR UPDATE";
+
+ queryString += ";";
+
+ q.prepare (queryString);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading budgets")));
+ QMap<QString, MyMoneyBudget> budgets;
+ while (q.next()) {
+ MyMoneyDbTable::field_iterator ft = t.begin();
+ int i = 0;
+ QDomDocument d;
+ while (ft != t.end()) {
+ CASE(XML) d.setContent(GETSTRING, false);
+ ++ft; ++i;
+ }
+ QDomNode child = d.firstChild();
+ child = child.firstChild();
+ MyMoneyBudget budget (child.toElement());
+ budgets.insert(budget.id(), budget);
+ signalProgress(++progress, 0);
+ }
+ return budgets;
+}
+
+void MyMoneyStorageSql::readBudgets(void) {
+ m_storage->loadBudgets(fetchBudgets());
+}
+
+const MyMoneyKeyValueContainer MyMoneyStorageSql::readKeyValuePairs (const QString& kvpType, const QString& kvpId) const {
+ DBG("*** Entering MyMoneyStorageSql::readKeyValuePairs");
+ MyMoneyKeyValueContainer list;
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ q.prepare ("SELECT kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type and kvpId = :id;");
+ q.bindValue(":type", kvpType);
+ q.bindValue(":id", kvpId);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Kvp for %1 %2").arg(kvpType)
+ .arg(kvpId)));
+ while (q.next()) list.setValue(q.value(0).toString(), q.value(1).toString());
+ return (list);
+}
+
+const QMap<QString, MyMoneyKeyValueContainer> MyMoneyStorageSql::readKeyValuePairs (const QString& kvpType, const QStringList& kvpIdList) const {
+ DBG("*** Entering MyMoneyStorageSql::readKeyValuePairs");
+ QMap<QString, MyMoneyKeyValueContainer> retval;
+
+ MyMoneySqlQuery q(const_cast <MyMoneyStorageSql*> (this));
+ QString query ("SELECT kvpId, kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type");
+
+ if (!kvpIdList.empty()) {
+ query += " and kvpId IN ('" + kvpIdList.join("', '") + "')";
+ }
+
+ query += " order by kvpId;";
+ q.prepare (query);
+ q.bindValue(":type", kvpType);
+ if (!q.exec()) throw new MYMONEYEXCEPTION(buildError (q, __func__, QString("reading Kvp List for %1").arg(kvpType)));
+ while (q.next()) {
+ retval [q.value(0).toString()].setValue(q.value(1).toString(), q.value(2).toString());
+ }
+
+ return (retval);
+}
+
+long unsigned MyMoneyStorageSql::getNextBudgetId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdBudgets;
+}
+
+long unsigned MyMoneyStorageSql::getNextAccountId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdAccounts;
+}
+
+long unsigned MyMoneyStorageSql::getNextInstitutionId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdInstitutions;
+}
+
+long unsigned MyMoneyStorageSql::getNextPayeeId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdPayees;
+}
+
+long unsigned MyMoneyStorageSql::getNextReportId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdReports;
+}
+
+long unsigned MyMoneyStorageSql::getNextScheduleId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdSchedules;
+}
+
+long unsigned MyMoneyStorageSql::getNextSecurityId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdSecurities;
+}
+
+long unsigned MyMoneyStorageSql::getNextTransactionId() const {
+ const_cast <MyMoneyStorageSql*> (this)->readFileInfo();
+ return m_hiIdTransactions;
+}
+
+long unsigned MyMoneyStorageSql::incrementBudgetId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiBudgetId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiBudgetId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdBudgets = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementAccountId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiAccountId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiAccountId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdAccounts = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementInstitutionId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiInstitutionId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiInstitutionId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdInstitutions = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementPayeeId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiPayeeId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiPayeeId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdPayees = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementReportId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiReportId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiReportId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdReports = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementScheduleId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiScheduleId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiScheduleId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdSchedules = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementSecurityId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiSecurityId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiSecurityId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdSecurities = returnValue;
+ return returnValue;
+}
+
+long unsigned MyMoneyStorageSql::incrementTransactionId() {
+ MyMoneySqlQuery q(this);
+
+ startCommitUnit (__func__);
+ q.prepare("SELECT hiTransactionId FROM kmmFileInfo FOR UPDATE");
+ q.exec();
+ q.next();
+ long unsigned returnValue = (unsigned long) q.value(0).toULongLong();
+ ++returnValue;
+ q.prepare("UPDATE kmmFileInfo SET hiTransactionId = " + QString::number(returnValue));
+ q.exec();
+ endCommitUnit (__func__);
+ m_hiIdTransactions = returnValue;
+ return returnValue;
+}
+
+void MyMoneyStorageSql::loadAccountId(const unsigned long& id)
+{
+ m_hiIdAccounts = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadTransactionId(const unsigned long& id)
+{
+ m_hiIdTransactions = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadPayeeId(const unsigned long& id)
+{
+ m_hiIdPayees = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadInstitutionId(const unsigned long& id)
+{
+ m_hiIdInstitutions = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadScheduleId(const unsigned long& id)
+{
+ m_hiIdSchedules = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadSecurityId(const unsigned long& id)
+{
+ m_hiIdSecurities = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadReportId(const unsigned long& id)
+{
+ m_hiIdReports = id;
+ writeFileInfo();
+}
+
+void MyMoneyStorageSql::loadBudgetId(const unsigned long& id)
+{
+ m_hiIdBudgets = id;
+ writeFileInfo();
+}
+
+//****************************************************
+long unsigned MyMoneyStorageSql::calcHighId
+ (const long unsigned& i, const QString& id) {
+ DBG("*** Entering MyMoneyStorageSql::calcHighId");
+ QString nid = id;
+ long unsigned high = (unsigned long) nid.replace(QRegExp("[A-Z]*"), "").toULongLong();
+ return std::max(high, i);
+}
+
+void MyMoneyStorageSql::setProgressCallback(void(*callback)(int, int, const QString&)) {
+ m_progressCallback = callback;
+}
+
+void MyMoneyStorageSql::signalProgress(int current, int total, const QString& msg) const {
+ if (m_progressCallback != 0)
+ (*m_progressCallback)(current, total, msg);
+}
+
+// **************************** Error display routine *******************************
+QString& MyMoneyStorageSql::buildError (const QSqlQuery& q, const QString& function, const QString& message) const {
+ QString s = QString("Error in function %1 : %2").arg(function).arg(message);
+ QSqlError e = lastError();
+ s += QString ("\nDriver = %1, Host = %2, User = %3, Database = %4")
+ .arg(driverName()).arg(hostName()).arg(userName()).arg(databaseName());
+ s += QString ("\nDriver Error: %1").arg(e.driverText());
+ s += QString ("\nDatabase Error No %1: %2").arg(e.number()).arg(e.databaseText());
+ e = q.lastError();
+ s += QString ("\nExecuted: %1").arg(q.executedQuery());
+ s += QString ("\nQuery error No %1: %2").arg(e.number()).arg(e.text());
+
+ const_cast <MyMoneyStorageSql*> (this)->m_error = s;
+ qDebug("%s", s.ascii());
+ const_cast <MyMoneyStorageSql*> (this)->cancelCommitUnit(function);
+ return (const_cast <MyMoneyStorageSql*> (this)->m_error);
+}
+
+// ************************* Build table descriptions ****************************
+MyMoneyDbDef::MyMoneyDbDef () {
+ FileInfo();
+ Institutions();
+ Payees();
+ Accounts();
+ Transactions();
+ Splits();
+ KeyValuePairs();
+ Schedules();
+ SchedulePaymentHistory();
+ Securities();
+ Prices();
+ Currencies();
+ Reports();
+ Budgets();
+ Balances();
+}
+
+/* PRIMARYKEY - these fields combine to form a unique key field on which the db will create an index
+ NOTNULL - this field should never be null
+ UNSIGNED - for numeric types, indicates the field is UNSIGNED
+ ?ISKEY - where there is no primary key, these fields can be used to uniquely identify a record
+ Default is that a field is not a part of a primary key, nullable, and if numeric, signed */
+
+#define PRIMARYKEY true
+#define NOTNULL true
+#define UNSIGNED false
+//#define ISKEY true
+
+void MyMoneyDbDef::FileInfo(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("version", "varchar(16)"));
+ fields.append(new MyMoneyDbColumn("created", "date"));
+ fields.append(new MyMoneyDbColumn("lastModified", "date"));
+ fields.append(new MyMoneyDbColumn("baseCurrency", "char(3)"));
+ fields.append(new MyMoneyDbIntColumn("institutions", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("accounts", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("payees", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("transactions", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("splits", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("securities", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("prices", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("currencies", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("schedules", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("reports", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("kvps", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbColumn("dateRangeStart", "date"));
+ fields.append(new MyMoneyDbColumn("dateRangeEnd", "date"));
+ fields.append(new MyMoneyDbIntColumn("hiInstitutionId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiPayeeId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiAccountId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiTransactionId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiScheduleId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiSecurityId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiReportId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbColumn("encryptData", "varchar(255)"));
+ fields.append(new MyMoneyDbColumn("updateInProgress", "char(1)"));
+ fields.append(new MyMoneyDbIntColumn("budgets", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("hiBudgetId", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ fields.append(new MyMoneyDbColumn("logonUser", "varchar(255)"));
+ fields.append(new MyMoneyDbDatetimeColumn("logonAt"));
+ fields.append(new MyMoneyDbIntColumn("fixLevel",
+ MyMoneyDbIntColumn::MEDIUM, UNSIGNED));
+ MyMoneyDbTable t("kmmFileInfo", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Institutions(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("name", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("manager"));
+ fields.append(new MyMoneyDbTextColumn("routingCode"));
+ fields.append(new MyMoneyDbTextColumn("addressStreet"));
+ fields.append(new MyMoneyDbTextColumn("addressCity"));
+ fields.append(new MyMoneyDbTextColumn("addressZipcode"));
+ fields.append(new MyMoneyDbTextColumn("telephone"));
+ MyMoneyDbTable t("kmmInstitutions", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Payees(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("name"));
+ fields.append(new MyMoneyDbTextColumn("reference"));
+ fields.append(new MyMoneyDbTextColumn("email"));
+ fields.append(new MyMoneyDbTextColumn("addressStreet"));
+ fields.append(new MyMoneyDbTextColumn("addressCity"));
+ fields.append(new MyMoneyDbTextColumn("addressZipcode"));
+ fields.append(new MyMoneyDbTextColumn("addressState"));
+ fields.append(new MyMoneyDbTextColumn("telephone"));
+ fields.append(new MyMoneyDbTextColumn("notes", MyMoneyDbTextColumn::LONG));
+ fields.append(new MyMoneyDbColumn("defaultAccountId", "varchar(32)"));
+ fields.append(new MyMoneyDbIntColumn("matchData", MyMoneyDbIntColumn::TINY, UNSIGNED));
+ fields.append(new MyMoneyDbColumn("matchIgnoreCase", "char(1)"));
+ fields.append(new MyMoneyDbTextColumn("matchKeys"));
+ MyMoneyDbTable t("kmmPayees", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Accounts(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("institutionId", "varchar(32)"));
+ fields.append(new MyMoneyDbColumn("parentId", "varchar(32)"));
+ fields.append(new MyMoneyDbDatetimeColumn("lastReconciled"));
+ fields.append(new MyMoneyDbDatetimeColumn("lastModified"));
+ fields.append(new MyMoneyDbColumn("openingDate", "date"));
+ fields.append(new MyMoneyDbTextColumn("accountNumber"));
+ fields.append(new MyMoneyDbColumn("accountType", "varchar(16)", false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("accountTypeString"));
+ fields.append(new MyMoneyDbColumn("isStockAccount", "char(1)"));
+ fields.append(new MyMoneyDbTextColumn("accountName"));
+ fields.append(new MyMoneyDbTextColumn("description"));
+ fields.append(new MyMoneyDbColumn("currencyId", "varchar(32)"));
+ fields.append(new MyMoneyDbTextColumn("balance"));
+ fields.append(new MyMoneyDbTextColumn("balanceFormatted"));
+ fields.append(new MyMoneyDbIntColumn("transactionCount", MyMoneyDbIntColumn::BIG, UNSIGNED));
+ MyMoneyDbTable t("kmmAccounts", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Transactions(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("txType", "char(1)"));
+ fields.append(new MyMoneyDbDatetimeColumn("postDate"));
+ fields.append(new MyMoneyDbTextColumn("memo"));
+ fields.append(new MyMoneyDbDatetimeColumn("entryDate"));
+ fields.append(new MyMoneyDbColumn("currencyId", "char(3)"));
+ fields.append(new MyMoneyDbTextColumn("bankId"));
+ MyMoneyDbTable t("kmmTransactions", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Splits(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("transactionId", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("txType", "char(1)"));
+ fields.append(new MyMoneyDbIntColumn("splitId", MyMoneyDbIntColumn::SMALL, UNSIGNED, PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("payeeId", "varchar(32)"));
+ fields.append(new MyMoneyDbDatetimeColumn("reconcileDate"));
+ fields.append(new MyMoneyDbColumn("action", "varchar(16)"));
+ fields.append(new MyMoneyDbColumn("reconcileFlag", "char(1)"));
+ fields.append(new MyMoneyDbTextColumn("value", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("valueFormatted", "text"));
+ fields.append(new MyMoneyDbTextColumn("shares", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("sharesFormatted"));
+ fields.append(new MyMoneyDbTextColumn("price", MyMoneyDbTextColumn::NORMAL, false));
+ fields.append(new MyMoneyDbTextColumn("priceFormatted"));
+ fields.append(new MyMoneyDbTextColumn("memo"));
+ fields.append(new MyMoneyDbColumn("accountId", "varchar(32)", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("checkNumber", "varchar(32)"));
+ fields.append(new MyMoneyDbDatetimeColumn("postDate"));
+ fields.append(new MyMoneyDbTextColumn("bankId"));
+ MyMoneyDbTable t("kmmSplits", fields);
+ QStringList list;
+ list << "accountId" << "txType";
+ t.addIndex("kmmSplitsaccount_type", list, false);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::KeyValuePairs(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("kvpType", "varchar(16)", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("kvpId", "varchar(32)"));
+ fields.append(new MyMoneyDbColumn("kvpKey", "varchar(255)", false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("kvpData"));
+ MyMoneyDbTable t("kmmKeyValuePairs", fields);
+ QStringList list;
+ list << "kvpType" << "kvpId";
+ t.addIndex("type_id", list, false);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Schedules(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("name", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbIntColumn("type", MyMoneyDbIntColumn::TINY, UNSIGNED, false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("typeString"));
+ fields.append(new MyMoneyDbIntColumn("occurence", MyMoneyDbIntColumn::SMALL, UNSIGNED, false,
+ NOTNULL));
+ fields.append(new MyMoneyDbIntColumn("occurenceMultiplier", MyMoneyDbIntColumn::SMALL, UNSIGNED,
+ false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("occurenceString"));
+ fields.append(new MyMoneyDbIntColumn("paymentType", MyMoneyDbIntColumn::TINY, UNSIGNED));
+ fields.append(new MyMoneyDbTextColumn("paymentTypeString", MyMoneyDbTextColumn::LONG));
+ fields.append(new MyMoneyDbColumn("startDate", "date", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("endDate", "date"));
+ fields.append(new MyMoneyDbColumn("fixed", "char(1)", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("autoEnter", "char(1)", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("lastPayment", "date"));
+ fields.append(new MyMoneyDbColumn("nextPaymentDue", "date"));
+ fields.append(new MyMoneyDbIntColumn("weekendOption", MyMoneyDbIntColumn::TINY, UNSIGNED, false,
+ NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("weekendOptionString"));
+ MyMoneyDbTable t("kmmSchedules", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::SchedulePaymentHistory(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("schedId", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("payDate", "date", PRIMARYKEY, NOTNULL));
+ MyMoneyDbTable t("kmmSchedulePaymentHistory", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Securities(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("name", "text", false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("symbol"));
+ fields.append(new MyMoneyDbIntColumn("type", MyMoneyDbIntColumn::SMALL, UNSIGNED, false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("typeString"));
+ fields.append(new MyMoneyDbColumn("smallestAccountFraction", "varchar(24)"));
+ fields.append(new MyMoneyDbTextColumn("tradingMarket"));
+ fields.append(new MyMoneyDbColumn("tradingCurrency", "char(3)"));
+ MyMoneyDbTable t("kmmSecurities", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Prices(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("fromId", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("toId", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("priceDate", "date", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("price", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("priceFormatted"));
+ fields.append(new MyMoneyDbTextColumn("priceSource"));
+ MyMoneyDbTable t("kmmPrices", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Currencies(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("ISOcode", "char(3)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("name", MyMoneyDbTextColumn::NORMAL, false, NOTNULL));
+ fields.append(new MyMoneyDbIntColumn("type", MyMoneyDbIntColumn::SMALL, UNSIGNED));
+ fields.append(new MyMoneyDbTextColumn("typeString"));
+ fields.append(new MyMoneyDbIntColumn("symbol1", MyMoneyDbIntColumn::SMALL, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("symbol2", MyMoneyDbIntColumn::SMALL, UNSIGNED));
+ fields.append(new MyMoneyDbIntColumn("symbol3", MyMoneyDbIntColumn::SMALL, UNSIGNED));
+ fields.append(new MyMoneyDbColumn("symbolString", "varchar(255)"));
+ fields.append(new MyMoneyDbColumn("partsPerUnit", "varchar(24)"));
+ fields.append(new MyMoneyDbColumn("smallestCashFraction", "varchar(24)"));
+ fields.append(new MyMoneyDbColumn("smallestAccountFraction", "varchar(24)"));
+ MyMoneyDbTable t("kmmCurrencies", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Reports(void) {
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("name", "varchar(255)", false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("XML", MyMoneyDbTextColumn::LONG));
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ MyMoneyDbTable t("kmmReportConfig", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Budgets(void){
+ QValueList<KSharedPtr <MyMoneyDbColumn> > fields;
+ fields.append(new MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL));
+ fields.append(new MyMoneyDbColumn("name", "text", false, NOTNULL));
+ fields.append(new MyMoneyDbColumn("start", "date", false, NOTNULL));
+ fields.append(new MyMoneyDbTextColumn("XML", MyMoneyDbTextColumn::LONG));
+ MyMoneyDbTable t("kmmBudgetConfig", fields);
+ t.buildSQLStrings();
+ m_tables[t.name()] = t;
+}
+
+void MyMoneyDbDef::Balances(void){
+ MyMoneyDbView v("kmmBalances", "CREATE VIEW kmmBalances AS "
+ "SELECT kmmAccounts.id AS id, kmmAccounts.currencyId, "
+ "kmmSplits.txType, kmmSplits.value, kmmSplits.shares, "
+ "kmmSplits.postDate AS balDate, "
+ "kmmTransactions.currencyId AS txCurrencyId "
+ "FROM kmmAccounts, kmmSplits, kmmTransactions "
+ "WHERE kmmSplits.txType = 'N' "
+ "AND kmmSplits.accountId = kmmAccounts.id "
+ "AND kmmSplits.transactionId = kmmTransactions.id;");
+ m_views[v.name()] = v;
+}
+
+// function to write create SQL to a stream
+const QString MyMoneyDbDef::generateSQL (const QString& driver) const {
+ QString retval;
+ databaseTypeE dbType = m_drivers.driverToType(driver);
+ table_iterator tt = tableBegin();
+ while (tt != tableEnd()) {
+ retval += (*tt).generateCreateSQL(dbType) + '\n';
+ ++tt;
+ }
+ view_iterator vt = viewBegin();
+ while (vt != viewEnd()) {
+ retval += (*vt).createString() + '\n';
+ ++vt;
+ }
+ retval += '\n';
+
+ MyMoneyDbTable fi = m_tables["kmmFileInfo"];
+ QString qs = fi.insertString();
+ MyMoneyDbTable::field_iterator fit;
+ for (fit = fi.begin(); fit != fi.end(); ++fit) {
+ QString toReplace = (*fit)->name();
+ toReplace.prepend(':');
+ QString replace = "NULL";
+ if ((*fit)->name() == "version")
+ replace = QString::number(m_currentVersion);
+ if ((*fit)->name() == "fixLevel")
+ replace = QString::number
+ (MyMoneyFile::instance()->storage()->currentFixVersion());
+ if ((*fit)->name() == "created")
+ replace = QDate::currentDate().toString(Qt::ISODate);
+ if ((*fit)->name() == "lastModified")
+ replace = QDate::currentDate().toString(Qt::ISODate);
+ if ((*fit)->name() == "updateInProgress")
+ replace = enclose("N");
+ qs.replace(toReplace, replace);
+ }
+ qs += "\n\n";
+ retval += qs;
+
+ qs = QString();
+ unsigned int i;
+ QValueList<MyMoneyAccount> stdList;
+ stdList.append (MyMoneyFile::instance()->asset());
+ stdList.append (MyMoneyFile::instance()->equity());
+ stdList.append (MyMoneyFile::instance()->expense());
+ stdList.append (MyMoneyFile::instance()->income());
+ stdList.append (MyMoneyFile::instance()->liability());
+ for (i = 0; i < stdList.count(); ++i) {
+ MyMoneyAccount* pac = &stdList[i];
+ MyMoneyDbTable ac = m_tables["kmmAccounts"];
+ qs = ac.insertString();
+ MyMoneyDbTable::field_iterator act;
+ // do the following in reverse so the 'formatted' fields are
+ // correctly handled.
+ // Hmm, how does one use a QValueListIterator in reverse
+ // It'll be okay in Qt4 with QListIterator
+ for (act = ac.end(), --act; act != ac.begin(); --act) {
+ QString toReplace = (*act)->name();
+ toReplace.prepend(':');
+ QString replace = "NULL";
+ if ((*act)->name() == "accountType")
+ replace = QString::number(pac->accountType());
+ if ((*act)->name() == "accountTypeString")
+ replace = enclose(pac->name());
+ if ((*act)->name() == "isStockAccount")
+ replace = enclose("N");
+ if ((*act)->name() == "accountName")
+ replace = enclose(pac->name());
+ qs.replace(toReplace, replace);
+ }
+ qs.replace (":id", enclose(pac->id())); // a real kludge
+ qs += "\n\n";
+ retval += qs;
+ }
+ return retval;
+}
+
+//*****************************************************************************
+
+void MyMoneyDbTable::addIndex(const QString& name, const QStringList& columns, bool unique) {
+ m_indices.push_back (MyMoneyDbIndex (m_name, name, columns, unique));
+}
+
+void MyMoneyDbTable::buildSQLStrings (void) {
+ // build fixed SQL strings for this table
+ // build the insert string with placeholders for each field
+ QString qs = QString("INSERT INTO %1 (").arg(name());
+ QString ws = ") VALUES (";
+ field_iterator ft = m_fields.begin();
+ while (ft != m_fields.end()) {
+ qs += QString("%1, ").arg((*ft)->name());
+ ws += QString(":%1, ").arg((*ft)->name());
+ ++ft;
+ }
+ qs = qs.left(qs.length() - 2);
+ ws = ws.left(ws.length() - 2);
+ m_insertString = qs + ws + ");";
+ // build a 'select all' string (select * is deprecated)
+ // don't terminate with semicolon coz we may want a where or order clause
+ m_selectAllString = "SELECT " + columnList() + " FROM " + name();;
+
+ // build an update string; key fields go in the where clause
+ qs = "UPDATE " + name() + " SET ";
+ ws = QString();
+ ft = m_fields.begin();
+ while (ft != m_fields.end()) {
+ if ((*ft)->isPrimaryKey()) {
+ if (!ws.isEmpty()) ws += " AND ";
+ ws += QString("%1 = :%2").arg((*ft)->name()).arg((*ft)->name());
+ } else {
+ qs += QString("%1 = :%2, ").arg((*ft)->name()).arg((*ft)->name());
+ }
+ ++ft;
+ }
+ qs = qs.left(qs.length() - 2);
+ if (!ws.isEmpty()) qs += " WHERE " + ws;
+ m_updateString = qs + ";";
+ // build a delete string; where clause as for update
+ qs = "DELETE FROM " + name();
+ if (!ws.isEmpty()) qs += " WHERE " + ws;
+ m_deleteString = qs + ";";
+ }
+
+const QString MyMoneyDbTable::columnList() const {
+ field_iterator ft = m_fields.begin();
+ QString qs;
+ ft = m_fields.begin();
+ while (ft != m_fields.end()) {
+ qs += QString("%1, ").arg((*ft)->name());
+ ++ft;
+ }
+ return (qs.left(qs.length() - 2));
+}
+
+const QString MyMoneyDbTable::generateCreateSQL (databaseTypeE dbType) const {
+ QString qs = QString("CREATE TABLE %1 (").arg(name());
+ QString pkey;
+ for (field_iterator it = m_fields.begin(); it != m_fields.end(); ++it) {
+ qs += (*it)->generateDDL (dbType) + ", ";
+ if ((*it)->isPrimaryKey ())
+ pkey += (*it)->name () + ", ";
+ }
+
+ if (!pkey.isEmpty()) {
+ qs += "PRIMARY KEY (" + pkey;
+ qs = qs.left(qs.length() -2) + "))";
+ } else {
+ qs = qs.left(qs.length() -2) + ")";
+ }
+
+ if (dbType == Mysql)
+ qs += " ENGINE = InnoDB;\n";
+ else
+ qs += ";\n";
+
+ for (index_iterator ii = m_indices.begin(); ii != m_indices.end(); ++ii) {
+ qs += (*ii).generateDDL(dbType);
+ }
+ return qs;
+}
+
+const QString MyMoneyDbTable::dropPrimaryKeyString(databaseTypeE dbType) const
+{
+ if (dbType == Mysql || dbType == Oracle8)
+ return "ALTER TABLE " + m_name + " DROP PRIMARY KEY;";
+ else if (dbType == Postgresql)
+ return "ALTER TABLE " + m_name + " DROP CONSTRAINT " + m_name + "_pkey;";
+ else if (dbType == Sqlite3)
+ return "";
+
+ return "";
+}
+
+const QString MyMoneyDbTable::modifyColumnString(databaseTypeE dbType, const QString& columnName, const MyMoneyDbColumn& newDef) const {
+ QString qs = "ALTER TABLE " + m_name + " ";
+ if (dbType == Mysql)
+ qs += "CHANGE " + columnName + " " + newDef.generateDDL(dbType);
+ else if (dbType == Postgresql)
+ qs += "ALTER COLUMN " + columnName + " TYPE " + newDef.generateDDL(dbType).section(' ', 1);
+ else if (dbType == Sqlite3)
+ qs = "";
+ else if (dbType == Oracle8)
+ qs = "MODIFY " + columnName + " " + newDef.generateDDL(dbType);
+
+ return qs;
+}
+
+//*****************************************************************************
+const QString MyMoneyDbIndex::generateDDL (databaseTypeE dbType) const
+{
+ Q_UNUSED(dbType);
+
+ QString qs = "CREATE ";
+
+ if (m_unique)
+ qs += "UNIQUE ";
+
+ qs += "INDEX " + m_table + "_" + m_name + "_idx ON "
+ + m_table + " (";
+
+ // The following should probably be revised. MySQL supports an index on
+ // partial columns, but not on a function. Postgres supports an index on
+ // the result of an SQL function, but not a partial column. There should be
+ // a way to merge these, and support other DBMSs like SQLite at the same time.
+ // For now, if we just use plain columns, this will work fine.
+ for (QStringList::const_iterator it = m_columns.begin(); it != m_columns.end(); ++it) {
+ qs += *it + ",";
+ }
+
+ qs = qs.left(qs.length() - 1) + ");\n";
+
+ return qs;
+}
+
+//*****************************************************************************
+// These are the actual column types.
+// TODO: consider changing all the else-if statements to driver classes.
+//
+
+MyMoneyDbColumn* MyMoneyDbColumn::clone () const
+{ return (new MyMoneyDbColumn (*this)); }
+
+MyMoneyDbIntColumn* MyMoneyDbIntColumn::clone () const
+{ return (new MyMoneyDbIntColumn (*this)); }
+
+MyMoneyDbDatetimeColumn* MyMoneyDbDatetimeColumn::clone () const
+{ return (new MyMoneyDbDatetimeColumn (*this)); }
+
+MyMoneyDbTextColumn* MyMoneyDbTextColumn::clone () const
+{ return (new MyMoneyDbTextColumn (*this)); }
+
+const QString MyMoneyDbColumn::generateDDL (databaseTypeE dbType) const
+{
+ Q_UNUSED(dbType);
+
+ QString qs = name() + " " + type();
+ if (isNotNull()) qs += " NOT NULL";
+ return qs;
+}
+
+const QString MyMoneyDbIntColumn::generateDDL (databaseTypeE dbType) const
+{
+ QString qs = name() + " ";
+
+ switch (m_type) {
+ case MyMoneyDbIntColumn::TINY:
+ if (dbType == Mysql || dbType == Sqlite3) {
+ qs += "tinyint ";
+ } else if (dbType == Postgresql) {
+ qs += "int2 ";
+ } else if (dbType == Db2) {
+ qs += "smallint ";
+ } else if (dbType == Oracle8) {
+ qs += "number(3) ";
+ } else {
+ // cross your fingers...
+ qs += "smallint ";
+ }
+ break;
+ case MyMoneyDbIntColumn::SMALL:
+ if (dbType == Mysql || dbType == Db2 || dbType == Sqlite3) {
+ qs += "smallint ";
+ } else if (dbType == Postgresql) {
+ qs += "int2 ";
+ } else if (dbType == Oracle8) {
+ qs += "number(5) ";
+ } else {
+ // cross your fingers...
+ qs += "smallint ";
+ }
+ break;
+ case MyMoneyDbIntColumn::MEDIUM:
+ if (dbType == Mysql || dbType == Db2) {
+ qs += "int ";
+ } else if (dbType == Postgresql) {
+ qs += "int4 ";
+ } else if (dbType == Sqlite3) {
+ qs += "integer ";
+ } else if (dbType == Oracle8) {
+ qs += "number(10) ";
+ } else {
+ // cross your fingers...
+ qs += "int ";
+ }
+ break;
+ case MyMoneyDbIntColumn::BIG:
+ if (dbType == Mysql || dbType == Db2 || dbType == Sqlite3) {
+ qs += "bigint ";
+ } else if (dbType == Postgresql) {
+ qs += "int8 ";
+ } else if (dbType == Oracle8) {
+ qs += "number(20) ";
+ } else {
+ // cross your fingers...
+ qs += "bigint ";
+ }
+ break;
+ default:
+ qs += "int ";
+ break;
+ }
+
+ if ((! m_isSigned) && (dbType == Mysql || dbType == Sqlite3)) {
+ qs += "unsigned ";
+ }
+
+ if (isNotNull()) qs += " NOT NULL";
+ if ((! m_isSigned) && (dbType == Postgresql)) {
+ qs += " check(" + name() + " >= 0)";
+ }
+ return qs;
+}
+
+const QString MyMoneyDbTextColumn::generateDDL (databaseTypeE dbType) const
+{
+ QString qs = name() + " ";
+
+ switch (m_type) {
+ case MyMoneyDbTextColumn::TINY:
+ if (dbType == Mysql || dbType == Sqlite3) {
+ qs += "tinytext ";
+ } else if (dbType == Postgresql) {
+ qs += "text ";
+ } else if (dbType == Db2) {
+ qs += "varchar(255) ";
+ } else if (dbType == Oracle8) {
+ qs += "varchar2(255) ";
+ } else {
+ // cross your fingers...
+ qs += "tinytext ";
+ }
+ break;
+ case MyMoneyDbTextColumn::NORMAL:
+ if (dbType == Mysql || dbType == Sqlite3 || dbType == Postgresql) {
+ qs += "text ";
+ } else if (dbType == Db2) {
+ qs += "clob(64K) ";
+ } else if (dbType == Oracle8) {
+ qs += "clob ";
+ } else {
+ // cross your fingers...
+ qs += "text ";
+ }
+ break;
+ case MyMoneyDbTextColumn::MEDIUM:
+ if (dbType == Mysql || dbType == Sqlite3 ) {
+ qs += "mediumtext ";
+ } else if (dbType == Postgresql) {
+ qs += "text ";
+ } else if (dbType == Db2) {
+ qs += "clob(16M) ";
+ } else if (dbType == Oracle8) {
+ qs += "clob ";
+ } else {
+ // cross your fingers...
+ qs += "mediumtext ";
+ }
+ break;
+ case MyMoneyDbTextColumn::LONG:
+ if (dbType == Mysql || dbType == Sqlite3 ) {
+ qs += "longtext ";
+ } else if (dbType == Postgresql) {
+ qs += "text ";
+ } else if (dbType == Db2) {
+ qs += "clob(2G) ";
+ } else if (dbType == Oracle8) {
+ qs += "clob ";
+ } else {
+ // cross your fingers...
+ qs += "longtext ";
+ }
+ break;
+ default:
+ if (dbType == Oracle8) {
+ qs += "clob ";
+ } else {
+ qs += "text ";
+ }
+ break;
+ }
+
+ if (isNotNull()) qs += " NOT NULL";
+
+ return qs;
+}
+
+const QString MyMoneyDbDatetimeColumn::generateDDL (databaseTypeE dbType) const
+{
+ QString qs = name() + " ";
+ if (dbType == Mysql || dbType == ODBC3) {
+ qs += "datetime ";
+ } else if (dbType == Postgresql || dbType == Db2 || dbType == Oracle8 || dbType == Sqlite3 ) {
+ qs += "timestamp ";
+ } else {
+ qs += "";
+ }
+ if (isNotNull()) qs += " NOT NULL";
+ return qs;
+}