f87ac1d6fb2c452394cbe1562225445c77a1233e
[quassel.git] / src / core / abstractsqlstorage.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "abstractsqlstorage.h"
22
23 #include <QMutexLocker>
24 #include <QSqlDriver>
25 #include <QSqlError>
26 #include <QSqlField>
27 #include <QSqlQuery>
28
29 #include "logmessage.h"
30 #include "quassel.h"
31
32 int AbstractSqlStorage::_nextConnectionId = 0;
33 AbstractSqlStorage::AbstractSqlStorage(QObject* parent)
34     : Storage(parent)
35 {}
36
37 AbstractSqlStorage::~AbstractSqlStorage()
38 {
39     // disconnect the connections, so their deletion is no longer interessting for us
40     QHash<QThread*, Connection*>::iterator conIter;
41     for (conIter = _connectionPool.begin(); conIter != _connectionPool.end(); ++conIter) {
42         QSqlDatabase::removeDatabase(conIter.value()->name());
43         disconnect(conIter.value(), nullptr, this, nullptr);
44     }
45 }
46
47 QSqlDatabase AbstractSqlStorage::logDb()
48 {
49     if (!_connectionPool.contains(QThread::currentThread()))
50         addConnectionToPool();
51
52     QSqlDatabase db = QSqlDatabase::database(_connectionPool[QThread::currentThread()]->name(), false);
53
54     if (!db.isOpen()) {
55         qWarning() << "Database connection" << displayName() << "for thread" << QThread::currentThread()
56                    << "was lost, attempting to reconnect...";
57         dbConnect(db);
58     }
59
60     return db;
61 }
62
63 void AbstractSqlStorage::addConnectionToPool()
64 {
65     QMutexLocker locker(&_connectionPoolMutex);
66     // we have to recheck if the connection pool already contains a connection for
67     // this thread. Since now (after the lock) we can only tell for sure
68     if (_connectionPool.contains(QThread::currentThread()))
69         return;
70
71     QThread* currentThread = QThread::currentThread();
72
73     int connectionId = _nextConnectionId++;
74
75     Connection* connection = new Connection(QLatin1String(QString("quassel_%1_con_%2").arg(driverName()).arg(connectionId).toLatin1()));
76     connection->moveToThread(currentThread);
77     connect(this, &QObject::destroyed, connection, &QObject::deleteLater);
78     connect(currentThread, &QObject::destroyed, connection, &QObject::deleteLater);
79     connect(connection, &QObject::destroyed, this, &AbstractSqlStorage::connectionDestroyed);
80     _connectionPool[currentThread] = connection;
81
82     QSqlDatabase db = QSqlDatabase::addDatabase(driverName(), connection->name());
83     db.setDatabaseName(databaseName());
84
85     if (!hostName().isEmpty())
86         db.setHostName(hostName());
87
88     if (port() != -1)
89         db.setPort(port());
90
91     if (!userName().isEmpty()) {
92         db.setUserName(userName());
93         db.setPassword(password());
94     }
95
96     dbConnect(db);
97 }
98
99 void AbstractSqlStorage::dbConnect(QSqlDatabase& db)
100 {
101     if (!db.open()) {
102         quWarning() << "Unable to open database" << displayName() << "for thread" << QThread::currentThread();
103         quWarning() << "-" << db.lastError().text();
104     }
105     else {
106         if (!initDbSession(db)) {
107             quWarning() << "Unable to initialize database" << displayName() << "for thread" << QThread::currentThread();
108             db.close();
109         }
110     }
111 }
112
113 Storage::State AbstractSqlStorage::init(const QVariantMap& settings, const QProcessEnvironment& environment, bool loadFromEnvironment)
114 {
115     setConnectionProperties(settings, environment, loadFromEnvironment);
116
117     _debug = Quassel::isOptionSet("debug");
118
119     QSqlDatabase db = logDb();
120     if (!db.isValid() || !db.isOpen())
121         return NotAvailable;
122
123     if (installedSchemaVersion() == -1) {
124         qCritical() << "Storage Schema is missing!";
125         return NeedsSetup;
126     }
127
128     if (installedSchemaVersion() > schemaVersion()) {
129         qCritical() << "Installed Schema is newer then any known Version.";
130         return NotAvailable;
131     }
132
133     if (installedSchemaVersion() < schemaVersion()) {
134         quInfo() << qPrintable(tr("Installed database schema (version %1) is not up to date. Upgrading to "
135                                   "version %2...  This may take a while for major upgrades.")
136                                    .arg(installedSchemaVersion())
137                                    .arg(schemaVersion()));
138         emit dbUpgradeInProgress(true);
139         auto upgradeResult = upgradeDb();
140         emit dbUpgradeInProgress(false);
141         if (!upgradeResult) {
142             qWarning() << qPrintable(tr("Upgrade failed..."));
143             return NotAvailable;
144         }
145         // Add a message when migration succeeds to avoid confusing folks by implying the schema upgrade failed if
146         // later functionality does not work.
147         quInfo() << qPrintable(tr("Installed database schema successfully upgraded to version %1.").arg(schemaVersion()));
148     }
149
150     quInfo() << qPrintable(displayName()) << "storage backend is ready. Schema version:" << installedSchemaVersion();
151     return IsReady;
152 }
153
154 QString AbstractSqlStorage::queryString(const QString& queryName, int version)
155 {
156     QFileInfo queryInfo;
157
158     // The current schema is stored in the root folder, while upgrade queries are stored in the
159     // 'versions/##' subfolders.
160     if (version == 0) {
161         // Use the current SQL schema, not a versioned request
162         queryInfo = QFileInfo(QString(":/SQL/%1/%2.sql").arg(displayName()).arg(queryName));
163         // If version is needed later, get it via version = schemaVersion();
164     }
165     else {
166         // Use the specified schema version, not the general folder
167         queryInfo = QFileInfo(QString(":/SQL/%1/version/%2/%3.sql").arg(displayName()).arg(version).arg(queryName));
168     }
169
170     if (!queryInfo.exists() || !queryInfo.isFile() || !queryInfo.isReadable()) {
171         qCritical() << "Unable to read SQL-Query" << queryName << "for engine" << displayName();
172         return QString();
173     }
174
175     QFile queryFile(queryInfo.filePath());
176     if (!queryFile.open(QIODevice::ReadOnly | QIODevice::Text))
177         return QString();
178     QString query = QTextStream(&queryFile).readAll();
179     queryFile.close();
180
181     return query.trimmed();
182 }
183
184 QStringList AbstractSqlStorage::setupQueries()
185 {
186     QStringList queries;
187     // The current schema is stored in the root folder, including setup scripts.
188     QDir dir = QDir(QString(":/SQL/%1/").arg(displayName()));
189     foreach (QFileInfo fileInfo, dir.entryInfoList(QStringList() << "setup*", QDir::NoFilter, QDir::Name)) {
190         queries << queryString(fileInfo.baseName());
191     }
192     return queries;
193 }
194
195 bool AbstractSqlStorage::setup(const QVariantMap& settings, const QProcessEnvironment& environment, bool loadFromEnvironment)
196 {
197     setConnectionProperties(settings, environment, loadFromEnvironment);
198     QSqlDatabase db = logDb();
199     if (!db.isOpen()) {
200         qCritical() << "Unable to setup Logging Backend!";
201         return false;
202     }
203
204     db.transaction();
205     foreach (QString queryString, setupQueries()) {
206         QSqlQuery query = db.exec(queryString);
207         if (!watchQuery(query)) {
208             qCritical() << "Unable to setup Logging Backend!";
209             db.rollback();
210             return false;
211         }
212     }
213     bool success = setupSchemaVersion(schemaVersion());
214     if (success)
215         db.commit();
216     else
217         db.rollback();
218     return success;
219 }
220
221 QStringList AbstractSqlStorage::upgradeQueries(int version)
222 {
223     QStringList queries;
224     // Upgrade queries are stored in the 'version/##' subfolders.
225     QDir dir = QDir(QString(":/SQL/%1/version/%2/").arg(displayName()).arg(version));
226     foreach (QFileInfo fileInfo, dir.entryInfoList(QStringList() << "upgrade*", QDir::NoFilter, QDir::Name)) {
227         queries << queryString(fileInfo.baseName(), version);
228     }
229     return queries;
230 }
231
232 bool AbstractSqlStorage::upgradeDb()
233 {
234     if (schemaVersion() <= installedSchemaVersion())
235         return true;
236
237     QSqlDatabase db = logDb();
238
239     // TODO: For databases that support it (e.g. almost only PostgreSQL), wrap upgrades in a
240     // transaction.  This will need careful testing of potential additional space requirements and
241     // any database modifications that might not be allowed in a transaction.
242
243     for (int ver = installedSchemaVersion() + 1; ver <= schemaVersion(); ver++) {
244         foreach (QString queryString, upgradeQueries(ver)) {
245             QSqlQuery query = db.exec(queryString);
246             if (!watchQuery(query)) {
247                 // Individual upgrade query failed, bail out
248                 qCritical() << "Unable to upgrade Logging Backend!  Upgrade query in schema version" << ver << "failed.";
249                 return false;
250             }
251         }
252
253         // Update the schema version for each intermediate step.  This ensures that any interrupted
254         // upgrades have a greater chance of resuming correctly after core restart.
255         //
256         // Almost all databases make single queries atomic (fully works or fully fails, no partial),
257         // and with many of the longest migrations being a single query, this makes upgrade
258         // interruptions much more likely to leave the database in a valid intermediate schema
259         // version.
260         if (!updateSchemaVersion(ver)) {
261             // Updating the schema version failed, bail out
262             qCritical() << "Unable to upgrade Logging Backend!  Setting schema version" << ver << "failed.";
263             return false;
264         }
265     }
266
267     // Update the schema version for the final step.  Split this out to offer more informative
268     // logging (though setting schema version really should not fail).
269     if (!updateSchemaVersion(schemaVersion())) {
270         // Updating the final schema version failed, bail out
271         qCritical() << "Unable to upgrade Logging Backend!  Setting final schema version" << schemaVersion() << "failed.";
272         return false;
273     }
274
275     // If we made it here, everything seems to have worked!
276     return true;
277 }
278
279 int AbstractSqlStorage::schemaVersion()
280 {
281     // returns the newest Schema Version!
282     // not the currently used one! (though it can be the same)
283     if (_schemaVersion > 0)
284         return _schemaVersion;
285
286     int version;
287     bool ok;
288     // Schema versions are stored in the 'version/##' subfolders.
289     QDir dir = QDir(QString(":/SQL/%1/version/").arg(displayName()));
290     foreach (QFileInfo fileInfo, dir.entryInfoList()) {
291         if (!fileInfo.isDir())
292             continue;
293
294         version = fileInfo.fileName().toInt(&ok);
295         if (!ok)
296             continue;
297
298         if (version > _schemaVersion)
299             _schemaVersion = version;
300     }
301     return _schemaVersion;
302 }
303
304 bool AbstractSqlStorage::watchQuery(QSqlQuery& query)
305 {
306     bool queryError = query.lastError().isValid();
307     if (queryError || _debug) {
308         if (queryError)
309             qCritical() << "unhandled Error in QSqlQuery!";
310         qCritical() << "                  last Query:\n" << qPrintable(query.lastQuery());
311         qCritical() << "              executed Query:\n" << qPrintable(query.executedQuery());
312         QVariantMap boundValues = query.boundValues();
313         QStringList valueStrings;
314         QVariantMap::const_iterator iter;
315         for (iter = boundValues.constBegin(); iter != boundValues.constEnd(); ++iter) {
316             QString value;
317             QSqlField field;
318             if (query.driver()) {
319                 // let the driver do the formatting
320                 field.setType(iter.value().type());
321                 if (iter.value().isNull())
322                     field.clear();
323                 else
324                     field.setValue(iter.value());
325                 value = query.driver()->formatValue(field);
326             }
327             else {
328                 switch (iter.value().type()) {
329                 case QVariant::Invalid:
330                     value = "NULL";
331                     break;
332                 case QVariant::Int:
333                     value = iter.value().toString();
334                     break;
335                 default:
336                     value = QString("'%1'").arg(iter.value().toString());
337                 }
338             }
339             valueStrings << QString("%1=%2").arg(iter.key(), value);
340         }
341         qCritical() << "                bound Values:" << qPrintable(valueStrings.join(", "));
342         qCritical() << "                Error Number:" << query.lastError().number();
343         qCritical() << "               Error Message:" << qPrintable(query.lastError().text());
344         qCritical() << "              Driver Message:" << qPrintable(query.lastError().driverText());
345         qCritical() << "                  DB Message:" << qPrintable(query.lastError().databaseText());
346
347         return !queryError;
348     }
349     return true;
350 }
351
352 void AbstractSqlStorage::connectionDestroyed()
353 {
354     QMutexLocker locker(&_connectionPoolMutex);
355     _connectionPool.remove(sender()->thread());
356 }
357
358 // ========================================
359 //  AbstractSqlStorage::Connection
360 // ========================================
361 AbstractSqlStorage::Connection::Connection(const QString& name, QObject* parent)
362     : QObject(parent)
363     , _name(name.toLatin1())
364 {}
365
366 AbstractSqlStorage::Connection::~Connection()
367 {
368     {
369         QSqlDatabase db = QSqlDatabase::database(name(), false);
370         if (db.isOpen()) {
371             db.commit();
372             db.close();
373         }
374     }
375     QSqlDatabase::removeDatabase(name());
376 }
377
378 // ========================================
379 //  AbstractSqlMigrator
380 // ========================================
381
382 void AbstractSqlMigrator::newQuery(const QString& query, QSqlDatabase db)
383 {
384     Q_ASSERT(!_query);
385     _query = new QSqlQuery(db);
386     _query->prepare(query);
387 }
388
389 void AbstractSqlMigrator::resetQuery()
390 {
391     delete _query;
392     _query = nullptr;
393 }
394
395 bool AbstractSqlMigrator::exec()
396 {
397     Q_ASSERT(_query);
398     _query->exec();
399     return !_query->lastError().isValid();
400 }
401
402 QString AbstractSqlMigrator::migrationObject(MigrationObject moType)
403 {
404     switch (moType) {
405     case QuasselUser:
406         return "QuasselUser";
407     case Sender:
408         return "Sender";
409     case Identity:
410         return "Identity";
411     case IdentityNick:
412         return "IdentityNick";
413     case Network:
414         return "Network";
415     case Buffer:
416         return "Buffer";
417     case Backlog:
418         return "Backlog";
419     case IrcServer:
420         return "IrcServer";
421     case UserSetting:
422         return "UserSetting";
423     case CoreState:
424         return "CoreState";
425     };
426     return QString();
427 }
428
429 QVariantList AbstractSqlMigrator::boundValues()
430 {
431     QVariantList values;
432     if (!_query)
433         return values;
434
435     int numValues = _query->boundValues().count();
436     for (int i = 0; i < numValues; i++) {
437         values << _query->boundValue(i);
438     }
439     return values;
440 }
441
442 void AbstractSqlMigrator::dumpStatus()
443 {
444     qWarning() << "  executed Query:";
445     qWarning() << qPrintable(executedQuery());
446     qWarning() << "  bound Values:";
447     QList<QVariant> list = boundValues();
448     for (int i = 0; i < list.size(); ++i)
449         qWarning() << i << ": " << list.at(i).toString().toLatin1().data();
450     qWarning() << "  Error Number:" << lastError().number();
451     qWarning() << "  Error Message:" << lastError().text();
452 }
453
454 // ========================================
455 //  AbstractSqlMigrationReader
456 // ========================================
457 AbstractSqlMigrationReader::AbstractSqlMigrationReader()
458     : AbstractSqlMigrator()
459 {}
460
461 bool AbstractSqlMigrationReader::migrateTo(AbstractSqlMigrationWriter* writer)
462 {
463     if (!transaction()) {
464         qWarning() << "AbstractSqlMigrationReader::migrateTo(): unable to start reader's transaction!";
465         return false;
466     }
467     if (!writer->transaction()) {
468         qWarning() << "AbstractSqlMigrationReader::migrateTo(): unable to start writer's transaction!";
469         rollback();  // close the reader transaction;
470         return false;
471     }
472
473     _writer = writer;
474
475     // due to the incompatibility across Migration objects we can't run this in a loop... :/
476     QuasselUserMO quasselUserMo;
477     if (!transferMo(QuasselUser, quasselUserMo))
478         return false;
479
480     IdentityMO identityMo;
481     if (!transferMo(Identity, identityMo))
482         return false;
483
484     IdentityNickMO identityNickMo;
485     if (!transferMo(IdentityNick, identityNickMo))
486         return false;
487
488     NetworkMO networkMo;
489     if (!transferMo(Network, networkMo))
490         return false;
491
492     BufferMO bufferMo;
493     if (!transferMo(Buffer, bufferMo))
494         return false;
495
496     SenderMO senderMo;
497     if (!transferMo(Sender, senderMo))
498         return false;
499
500     BacklogMO backlogMo;
501     if (!transferMo(Backlog, backlogMo))
502         return false;
503
504     IrcServerMO ircServerMo;
505     if (!transferMo(IrcServer, ircServerMo))
506         return false;
507
508     UserSettingMO userSettingMo;
509     if (!transferMo(UserSetting, userSettingMo))
510         return false;
511
512     CoreStateMO coreStateMO;
513     if (!transferMo(CoreState, coreStateMO))
514         return false;
515
516     if (!_writer->postProcess())
517         abortMigration();
518     return finalizeMigration();
519 }
520
521 void AbstractSqlMigrationReader::abortMigration(const QString& errorMsg)
522 {
523     qWarning() << "Migration Failed!";
524     if (!errorMsg.isNull()) {
525         qWarning() << qPrintable(errorMsg);
526     }
527     if (lastError().isValid()) {
528         qWarning() << "ReaderError:";
529         dumpStatus();
530     }
531
532     if (_writer->lastError().isValid()) {
533         qWarning() << "WriterError:";
534         _writer->dumpStatus();
535     }
536
537     rollback();
538     _writer->rollback();
539     _writer = nullptr;
540 }
541
542 bool AbstractSqlMigrationReader::finalizeMigration()
543 {
544     resetQuery();
545     _writer->resetQuery();
546
547     commit();
548     if (!_writer->commit()) {
549         _writer = nullptr;
550         return false;
551     }
552     _writer = nullptr;
553     return true;
554 }
555
556 template<typename T>
557 bool AbstractSqlMigrationReader::transferMo(MigrationObject moType, T& mo)
558 {
559     resetQuery();
560     _writer->resetQuery();
561
562     if (!prepareQuery(moType)) {
563         abortMigration(QString("AbstractSqlMigrationReader::migrateTo(): unable to prepare reader query of type %1!")
564                            .arg(AbstractSqlMigrator::migrationObject(moType)));
565         return false;
566     }
567     if (!_writer->prepareQuery(moType)) {
568         abortMigration(QString("AbstractSqlMigrationReader::migrateTo(): unable to prepare writer query of type %1!")
569                            .arg(AbstractSqlMigrator::migrationObject(moType)));
570         return false;
571     }
572
573     qDebug() << qPrintable(QString("Transferring %1...").arg(AbstractSqlMigrator::migrationObject(moType)));
574     int i = 0;
575     QFile file;
576     file.open(stdout, QIODevice::WriteOnly);
577
578     while (readMo(mo)) {
579         if (!_writer->writeMo(mo)) {
580             abortMigration(QString("AbstractSqlMigrationReader::transferMo(): unable to transfer Migratable Object of type %1!")
581                                .arg(AbstractSqlMigrator::migrationObject(moType)));
582             return false;
583         }
584         i++;
585         if (i % 1000 == 0) {
586             file.write("*");
587             file.flush();
588         }
589     }
590     if (i > 1000) {
591         file.write("\n");
592         file.flush();
593     }
594
595     qDebug() << "Done.";
596     return true;
597 }
598
599 uint qHash(const SenderData& key)
600 {
601     return qHash(QString(key.sender + "\n" + key.realname + "\n" + key.avatarurl));
602 }
603
604 bool operator==(const SenderData& a, const SenderData& b)
605 {
606     return a.sender == b.sender && a.realname == b.realname && a.avatarurl == b.avatarurl;
607 }