core: Workaround Qt 4 SQL bindValue() duplicates
[quassel.git] / src / core / sqlitestorage.cpp
index e40150c..8c2df4b 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2018 by the Quassel Project                        *
+ *   Copyright (C) 2005-2019 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -22,7 +22,7 @@
 
 #include <QtSql>
 
-#include "logger.h"
+#include "logmessage.h"
 #include "network.h"
 #include "quassel.h"
 
@@ -87,23 +87,40 @@ int SqliteStorage::installedSchemaVersion()
 }
 
 
-bool SqliteStorage::updateSchemaVersion(int newVersion)
+bool SqliteStorage::updateSchemaVersion(int newVersion, bool clearUpgradeStep)
 {
     // only used when there is a singlethread (during startup)
     // so we don't need locking here
-    QSqlQuery query(logDb());
+
+    QSqlDatabase db = logDb();
+
+    // Atomically update the schema version and clear the upgrade step, if specified
+    // Note: This will need reworked if "updateSchemaVersion" is ever called within a transaction.
+    db.transaction();
+
+    QSqlQuery query(db);
     query.prepare("UPDATE coreinfo SET value = :version WHERE key = 'schemaversion'");
     query.bindValue(":version", newVersion);
-    query.exec();
+    safeExec(query);
 
-    bool success = true;
-    if (query.lastError().isValid()) {
-        qCritical() << "SqliteStorage::updateSchemaVersion(int): Updating schema version failed!";
-        success = false;
+    if (!watchQuery(query)) {
+        qCritical() << "SqliteStorage::updateSchemaVersion(int, bool): Updating schema version failed!";
+        db.rollback();
+        return false;
     }
-    return success;
-}
 
+    if (clearUpgradeStep) {
+        // Try clearing the upgrade step if requested
+        if (!setSchemaVersionUpgradeStep("")) {
+            db.rollback();
+            return false;
+        }
+    }
+
+    // Successful, commit and return true
+    db.commit();
+    return true;
+}
 
 bool SqliteStorage::setupSchemaVersion(int version)
 {
@@ -123,6 +140,54 @@ bool SqliteStorage::setupSchemaVersion(int version)
 }
 
 
+QString SqliteStorage::schemaVersionUpgradeStep()
+{
+    // Only used when there is a singlethread (during startup), so we don't need locking here
+    QSqlQuery query(logDb());
+    query.prepare("SELECT value FROM coreinfo WHERE key = 'schemaupgradestep'");
+    safeExec(query);
+    watchQuery(query);
+    if (query.first())
+        return query.value(0).toString();
+
+    // Fall back to the default value
+    return AbstractSqlStorage::schemaVersionUpgradeStep();
+}
+
+
+bool SqliteStorage::setSchemaVersionUpgradeStep(QString upgradeQuery)
+{
+    // Only used when there is a singlethread (during startup), so we don't need locking here
+
+    // Intentionally do not wrap in a transaction so other functions can include multiple operations
+    QSqlQuery query(logDb());
+    query.prepare("UPDATE coreinfo SET value = :upgradestep WHERE key = 'schemaupgradestep'");
+    query.bindValue(":upgradestep", upgradeQuery);
+    safeExec(query);
+
+    // Don't wrap with watchQuery to avoid an alarming message in the log when the key is missing
+    // Make sure that the query didn't fail, and that some non-zero number of rows were affected
+    bool success = !query.lastError().isValid() && query.numRowsAffected() != 0;
+
+    if (!success) {
+        // The key might not exist (Quassel 0.13.0 and older).  Try inserting it...
+        query = QSqlQuery(logDb());
+        query.prepare("INSERT INTO coreinfo (key, value) VALUES ('schemaupgradestep', :upgradestep)");
+        query.bindValue(":upgradestep", upgradeQuery);
+        safeExec(query);
+
+        if (!watchQuery(query)) {
+            qCritical() << Q_FUNC_INFO << "Setting schema upgrade step failed!";
+            success = false;
+        }
+        else {
+            success = true;
+        }
+    }
+    return success;
+}
+
+
 UserId SqliteStorage::addUser(const QString &user, const QString &password, const QString &authenticator)
 {
     QSqlDatabase db = logDb();
@@ -1756,18 +1821,9 @@ bool SqliteStorage::logMessage(Message &msg)
     {
         QSqlQuery logMessageQuery(db);
         logMessageQuery.prepare(queryString("insert_message"));
-        // Store timestamp in seconds as 64-bit integer
-        //
-        // NOTE:  This is a loss of precision.  The database time column would need to store a
-        // fractional number to support toMSecsSinceEpoch(), or an upgrade step would need to
-        // convert all past times to milliseconds, multiplying by 1000.
-#if QT_VERSION >= 0x050800
-        logMessageQuery.bindValue(":time", msg.timestamp().toSecsSinceEpoch());
-#else
-        // toSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
-        // See https://doc.qt.io/qt-5/qdatetime.html#toMSecsSinceEpoch
-        logMessageQuery.bindValue(":time", (qint64)(msg.timestamp().toMSecsSinceEpoch() / 1000));
-#endif
+        // As of SQLite schema version 31, timestamps are stored in milliseconds instead of
+        // seconds.  This nets us more precision as well as simplifying 64-bit time.
+        logMessageQuery.bindValue(":time", msg.timestamp().toMSecsSinceEpoch());
         logMessageQuery.bindValue(":bufferid", msg.bufferInfo().bufferId().toInt());
         logMessageQuery.bindValue(":type", msg.type());
         logMessageQuery.bindValue(":flags", (int)msg.flags());
@@ -1849,19 +1905,9 @@ bool SqliteStorage::logMessages(MessageList &msgs)
         logMessageQuery.prepare(queryString("insert_message"));
         for (int i = 0; i < msgs.count(); i++) {
             Message &msg = msgs[i];
-            // Store timestamp in seconds as 64-bit integer
-            //
-            // NOTE:  This is a loss of precision.  The database time column would need to store a
-            // fractional number to support toMSecsSinceEpoch(), or an upgrade step would need to
-            // convert all past times to milliseconds, multiplying by 1000.
-#if QT_VERSION >= 0x050800
-            logMessageQuery.bindValue(":time", msg.timestamp().toSecsSinceEpoch());
-#else
-            // toSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
-            // See https://doc.qt.io/qt-5/qdatetime.html#toMSecsSinceEpoch
-            logMessageQuery.bindValue(":time",
-                                      (qint64)(msg.timestamp().toMSecsSinceEpoch() / 1000));
-#endif
+            // As of SQLite schema version 31, timestamps are stored in milliseconds instead of
+            // seconds.  This nets us more precision as well as simplifying 64-bit time.
+            logMessageQuery.bindValue(":time", msg.timestamp().toMSecsSinceEpoch());
             logMessageQuery.bindValue(":bufferid", msg.bufferInfo().bufferId().toInt());
             logMessageQuery.bindValue(":type", msg.type());
             logMessageQuery.bindValue(":flags", (int)msg.flags());
@@ -1933,15 +1979,20 @@ QList<Message> SqliteStorage::requestMsgs(UserId user, BufferId bufferId, MsgId
         QSqlQuery query(db);
         if (last == -1 && first == -1) {
             query.prepare(queryString("select_messagesNewestK"));
+            // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+            query.bindValue(":bufferidDup1", bufferId.toInt());
         }
         else if (last == -1) {
             query.prepare(queryString("select_messagesNewerThan"));
             query.bindValue(":firstmsg", first.toQint64());
+            // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+            query.bindValue(":bufferidDup1", bufferId.toInt());
         }
         else {
             query.prepare(queryString("select_messagesRange"));
             query.bindValue(":lastmsg", last.toQint64());
             query.bindValue(":firstmsg", first.toQint64());
+            // Workaround for Qt 4 QSqlQuery::bindValue() not needed, only has one ":bufferid"
         }
         query.bindValue(":bufferid", bufferId.toInt());
         query.bindValue(":limit", limit);
@@ -1951,19 +2002,9 @@ QList<Message> SqliteStorage::requestMsgs(UserId user, BufferId bufferId, MsgId
 
         while (query.next()) {
             Message msg(
-                // Read timestamp in seconds as 64-bit integer
-                //
-                // NOTE:  This is a loss of precision.  The database time column would need to store
-                // a fractional number to support fromMSecsSinceEpoch(), or an upgrade step would
-                // need to convert all past times to milliseconds, multiplying by 1000.
-#if QT_VERSION >= 0x050800
-                QDateTime::fromSecsSinceEpoch(query.value(1).toLongLong()),
-#else
-                // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for
-                // now.
-                // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
-                QDateTime::fromMSecsSinceEpoch((qint64)(query.value(1).toLongLong() * 1000)),
-#endif
+                // As of SQLite schema version 31, timestamps are stored in milliseconds instead of
+                // seconds.  This nets us more precision as well as simplifying 64-bit time.
+                QDateTime::fromMSecsSinceEpoch(query.value(1).toLongLong()),
                 bufferInfo,
                 (Message::Type)query.value(2).toInt(),
                 query.value(8).toString(),
@@ -2018,15 +2059,20 @@ QList<Message> SqliteStorage::requestMsgsFiltered(UserId user, BufferId bufferId
         QSqlQuery query(db);
         if (last == -1 && first == -1) {
             query.prepare(queryString("select_messagesNewestK_filtered"));
+            // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+            query.bindValue(":bufferidDup1", bufferId.toInt());
         }
         else if (last == -1) {
             query.prepare(queryString("select_messagesNewerThan_filtered"));
             query.bindValue(":firstmsg", first.toQint64());
+            // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+            query.bindValue(":bufferidDup1", bufferId.toInt());
         }
         else {
             query.prepare(queryString("select_messagesRange_filtered"));
             query.bindValue(":lastmsg", last.toQint64());
             query.bindValue(":firstmsg", first.toQint64());
+            // Workaround for Qt 4 QSqlQuery::bindValue() not needed, only has one ":bufferid"
         }
         query.bindValue(":bufferid", bufferId.toInt());
         query.bindValue(":limit", limit);
@@ -2034,27 +2080,18 @@ QList<Message> SqliteStorage::requestMsgsFiltered(UserId user, BufferId bufferId
         query.bindValue(":type", typeRaw);
         int flagsRaw = flags;
         query.bindValue(":flags", flagsRaw);
+        // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+        query.bindValue(":flagsDup1", flagsRaw);
 
         safeExec(query);
         watchQuery(query);
 
         while (query.next()) {
             Message msg(
-                        // Read timestamp in seconds as 64-bit integer
-                        //
-                        // NOTE:  This is a loss of precision.  The database time column would need
-                        // to store a fractional number to support fromMSecsSinceEpoch(), or an
-                        // upgrade step would need to convert all past times to milliseconds,
-                        // multiplying by 1000.
-#if QT_VERSION >= 0x050800
-                        QDateTime::fromSecsSinceEpoch(query.value(1).toLongLong()),
-#else
-                        // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to
-                        // seconds for now.
-                        // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
-                        QDateTime::fromMSecsSinceEpoch(
-                            (qint64)(query.value(1).toLongLong() * 1000)),
-#endif
+                        // As of SQLite schema version 31, timestamps are stored in milliseconds
+                        // instead of seconds.  This nets us more precision as well as simplifying
+                        // 64-bit time.
+                        QDateTime::fromMSecsSinceEpoch(query.value(1).toLongLong()),
                         bufferInfo,
                         (Message::Type)query.value(2).toInt(),
                         query.value(8).toString(),
@@ -2112,19 +2149,9 @@ QList<Message> SqliteStorage::requestAllMsgs(UserId user, MsgId first, MsgId las
 
         while (query.next()) {
             Message msg(
-                // Read timestamp in seconds as 64-bit integer
-                //
-                // NOTE:  This is a loss of precision.  The database time column would need to store
-                // a fractional number to support fromMSecsSinceEpoch(), or an upgrade step would
-                // need to convert all past times to milliseconds, multiplying by 1000.
-#if QT_VERSION >= 0x050800
-                QDateTime::fromSecsSinceEpoch(query.value(2).toLongLong()),
-#else
-                // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for
-                // now.
-                // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
-                QDateTime::fromMSecsSinceEpoch((qint64)(query.value(2).toLongLong() * 1000)),
-#endif
+                // As of SQLite schema version 31, timestamps are stored in milliseconds instead of
+                // seconds.  This nets us more precision as well as simplifying 64-bit time.
+                QDateTime::fromMSecsSinceEpoch(query.value(2).toLongLong()),
                 bufferInfoHash[query.value(1).toInt()],
                 (Message::Type)query.value(3).toInt(),
                 query.value(9).toString(),
@@ -2178,27 +2205,18 @@ QList<Message> SqliteStorage::requestAllMsgsFiltered(UserId user, MsgId first, M
         query.bindValue(":type", typeRaw);
         int flagsRaw = flags;
         query.bindValue(":flags", flagsRaw);
+        // Workaround for Qt 4 QSqlQuery::bindValue() not supporting repeated placeholder names
+        query.bindValue(":flagsDup1", flagsRaw);
         safeExec(query);
 
         watchQuery(query);
 
         while (query.next()) {
             Message msg(
-                        // Read timestamp in seconds as 64-bit integer
-                        //
-                        // NOTE:  This is a loss of precision.  The database time column would need
-                        // to store a fractional number to support fromMSecsSinceEpoch(), or an
-                        // upgrade step would need to convert all past times to milliseconds,
-                        // multiplying by 1000.
-#if QT_VERSION >= 0x050800
-                        QDateTime::fromSecsSinceEpoch(query.value(2).toLongLong()),
-#else
-                        // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to
-                        // seconds for now.
-                        // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
-                        QDateTime::fromMSecsSinceEpoch(
-                            (qint64)(query.value(2).toLongLong() * 1000)),
-#endif
+                        // As of SQLite schema version 31, timestamps are stored in milliseconds
+                        // instead of seconds.  This nets us more precision as well as simplifying
+                        // 64-bit time.
+                        QDateTime::fromMSecsSinceEpoch(query.value(2).toLongLong()),
                         bufferInfoHash[query.value(1).toInt()],
                         (Message::Type)query.value(3).toInt(),
                         query.value(9).toString(),
@@ -2239,25 +2257,6 @@ QMap<UserId, QString> SqliteStorage::getAllAuthUserNames()
 }
 
 
-QString SqliteStorage::getAuthUserName(UserId user) {
-    QString authusername;
-    QSqlQuery query(logDb());
-    query.prepare(queryString("select_authusername"));
-    query.bindValue(":userid", user.toInt());
-
-    lockForRead();
-    safeExec(query);
-    watchQuery(query);
-    unlock();
-
-    if (query.first()) {
-        authusername = query.value(0).toString();
-    }
-
-    return authusername;
-}
-
-
 QString SqliteStorage::backlogFile()
 {
     return Quassel::configDirPath() + "quassel-storage.sqlite";
@@ -2273,7 +2272,7 @@ bool SqliteStorage::safeExec(QSqlQuery &query, int retryCount)
 
     switch (query.lastError().number()) {
     case 5: // SQLITE_BUSY         5   /* The database file is locked */
-        [[clang::fallthrough]];
+        // fallthrough
     case 6: // SQLITE_LOCKED       6   /* A table in the database is locked */
         if (retryCount < _maxRetryCount)
             return safeExec(query, retryCount + 1);
@@ -2519,20 +2518,9 @@ bool SqliteMigrationReader::readMo(BacklogMO &backlog)
     }
 
     backlog.messageid = value(0).toLongLong();
-    // Read timestamp in seconds as 64-bit integer
-    //
-    // NOTE:  This is a loss of precision.  The database time column would need to store a
-    // fractional number to support fromMSecsSinceEpoch(), or an upgrade step would need to convert
-    // all past times to milliseconds, multiplying by 1000.
-#if QT_VERSION >= 0x050800
-    backlog.time = QDateTime::fromSecsSinceEpoch(value(1).toLongLong()).toUTC();
-#else
-    // fromSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for
-    // now.
-    // See https://doc.qt.io/qt-5/qdatetime.html#fromMSecsSinceEpoch
-    backlog.time = QDateTime::fromMSecsSinceEpoch((qint64)(value(1).toLongLong() * 1000)
-                                                  ).toUTC();
-#endif
+    // As of SQLite schema version 31, timestamps are stored in milliseconds instead of
+    // seconds.  This nets us more precision as well as simplifying 64-bit time.
+    backlog.time = QDateTime::fromMSecsSinceEpoch(value(1).toLongLong()).toUTC();
     backlog.bufferid = value(2).toInt();
     backlog.type = value(3).toInt();
     backlog.flags = value(4).toInt();