migration no longer eats memory
[quassel.git] / src / core / abstractsqlstorage.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-07 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  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20
21 #include "abstractsqlstorage.h"
22
23 #include "logger.h"
24
25 #include <QMutexLocker>
26 #include <QSqlError>
27 #include <QSqlQuery>
28
29 int AbstractSqlStorage::_nextConnectionId = 0;
30 AbstractSqlStorage::AbstractSqlStorage(QObject *parent)
31   : Storage(parent),
32     _schemaVersion(0)
33 {
34 }
35
36 AbstractSqlStorage::~AbstractSqlStorage() {
37   // disconnect the connections, so their deletion is no longer interessting for us
38   QHash<QThread *, Connection *>::iterator conIter;
39   for(conIter = _connectionPool.begin(); conIter != _connectionPool.end(); conIter++) {
40     QSqlDatabase::removeDatabase(conIter.value()->name());
41     disconnect(conIter.value(), 0, this, 0);
42   }
43 }
44
45 QSqlDatabase AbstractSqlStorage::logDb() {
46   if(!_connectionPool.contains(QThread::currentThread()))
47     addConnectionToPool();
48
49   return QSqlDatabase::database(_connectionPool[QThread::currentThread()]->name());
50 }
51
52 void AbstractSqlStorage::addConnectionToPool() {
53   QMutexLocker locker(&_connectionPoolMutex);
54   // we have to recheck if the connection pool already contains a connection for
55   // this thread. Since now (after the lock) we can only tell for sure
56   if(_connectionPool.contains(QThread::currentThread()))
57     return;
58
59   QThread *currentThread = QThread::currentThread();
60
61   int connectionId = _nextConnectionId++;
62
63   Connection *connection = new Connection(QLatin1String(QString("quassel_%1_con_%2").arg(driverName()).arg(connectionId).toLatin1()));
64   connection->moveToThread(currentThread);
65   connect(this, SIGNAL(destroyed()), connection, SLOT(deleteLater()));
66   connect(currentThread, SIGNAL(destroyed()), connection, SLOT(deleteLater()));
67   connect(connection, SIGNAL(destroyed()), this, SLOT(connectionDestroyed()));
68   _connectionPool[currentThread] = connection;
69
70   QSqlDatabase db = QSqlDatabase::addDatabase(driverName(), connection->name());
71   db.setDatabaseName(databaseName());
72
73   if(!hostName().isEmpty())
74     db.setHostName(hostName());
75
76   if(port() != -1)
77     db.setPort(port());
78
79   if(!userName().isEmpty()) {
80     db.setUserName(userName());
81     db.setPassword(password());
82   }
83
84   if(!db.open()) {
85     qWarning() << "Unable to open database" << displayName() << "for thread" << QThread::currentThread();
86     qWarning() << "-" << db.lastError().text();
87   }
88 }
89
90 Storage::State AbstractSqlStorage::init(const QVariantMap &settings) {
91   setConnectionProperties(settings);
92
93   QSqlDatabase db = logDb();
94   if(!db.isValid() || !db.isOpen())
95     return NotAvailable;
96
97   if(installedSchemaVersion() == -1) {
98     qCritical() << "Storage Schema is missing!";
99     return NeedsSetup;
100   }
101
102   if(installedSchemaVersion() > schemaVersion()) {
103     qCritical() << "Installed Schema is newer then any known Version.";
104     return NotAvailable;
105   }
106
107   if(installedSchemaVersion() < schemaVersion()) {
108     qWarning() << qPrintable(tr("Installed Schema (version %1) is not up to date. Upgrading to version %2...").arg(installedSchemaVersion()).arg(schemaVersion()));
109     if(!upgradeDb()) {
110       qWarning() << qPrintable(tr("Upgrade failed..."));
111       return NotAvailable;
112     }
113   }
114
115   quInfo() << qPrintable(displayName()) << "Storage Backend is ready. Quassel Schema Version:" << installedSchemaVersion();
116   return IsReady;
117 }
118
119 QString AbstractSqlStorage::queryString(const QString &queryName, int version) {
120   if(version == 0)
121     version = schemaVersion();
122
123   QFileInfo queryInfo(QString(":/SQL/%1/%2/%3.sql").arg(displayName()).arg(version).arg(queryName));
124   if(!queryInfo.exists() || !queryInfo.isFile() || !queryInfo.isReadable()) {
125     qCritical() << "Unable to read SQL-Query" << queryName << "for engine" << displayName();
126     return QString();
127   }
128
129   QFile queryFile(queryInfo.filePath());
130   if(!queryFile.open(QIODevice::ReadOnly | QIODevice::Text))
131     return QString();
132   QString query = QTextStream(&queryFile).readAll();
133   queryFile.close();
134
135   return query.trimmed();
136 }
137
138 QStringList AbstractSqlStorage::setupQueries() {
139   QStringList queries;
140   QDir dir = QDir(QString(":/SQL/%1/%2/").arg(displayName()).arg(schemaVersion()));
141   foreach(QFileInfo fileInfo, dir.entryInfoList(QStringList() << "setup*", QDir::NoFilter, QDir::Name)) {
142     queries << queryString(fileInfo.baseName());
143   }
144   return queries;
145 }
146
147 bool AbstractSqlStorage::setup(const QVariantMap &settings) {
148   setConnectionProperties(settings);
149   QSqlDatabase db = logDb();
150   if(!db.isOpen()) {
151     qCritical() << "Unable to setup Logging Backend!";
152     return false;
153   }
154
155   db.transaction();
156   foreach(QString queryString, setupQueries()) {
157     QSqlQuery query = db.exec(queryString);
158     if(!watchQuery(query)) {
159       qCritical() << "Unable to setup Logging Backend!";
160       db.rollback();
161       return false;
162     }
163   }
164   bool success = setupSchemaVersion(schemaVersion());
165   if(success)
166     db.commit();
167   else
168     db.rollback();
169   return success;
170 }
171
172 QStringList AbstractSqlStorage::upgradeQueries(int version) {
173   QStringList queries;
174   QDir dir = QDir(QString(":/SQL/%1/%2/").arg(displayName()).arg(version));
175   foreach(QFileInfo fileInfo, dir.entryInfoList(QStringList() << "upgrade*", QDir::NoFilter, QDir::Name)) {
176     queries << queryString(fileInfo.baseName(), version);
177   }
178   return queries;
179 }
180
181 bool AbstractSqlStorage::upgradeDb() {
182   if(schemaVersion() <= installedSchemaVersion())
183     return true;
184
185   QSqlDatabase db = logDb();
186
187   for(int ver = installedSchemaVersion() + 1; ver <= schemaVersion(); ver++) {
188     foreach(QString queryString, upgradeQueries(ver)) {
189       QSqlQuery query = db.exec(queryString);
190       if(!watchQuery(query)) {
191         qCritical() << "Unable to upgrade Logging Backend!";
192         return false;
193       }
194     }
195   }
196   return updateSchemaVersion(schemaVersion());
197 }
198
199
200 int AbstractSqlStorage::schemaVersion() {
201   // returns the newest Schema Version!
202   // not the currently used one! (though it can be the same)
203   if(_schemaVersion > 0)
204     return _schemaVersion;
205
206   int version;
207   bool ok;
208   QDir dir = QDir(":/SQL/" + displayName());
209   foreach(QFileInfo fileInfo, dir.entryInfoList()) {
210     if(!fileInfo.isDir())
211       continue;
212
213     version = fileInfo.fileName().toInt(&ok);
214     if(!ok)
215       continue;
216
217     if(version > _schemaVersion)
218       _schemaVersion = version;
219   }
220   return _schemaVersion;
221 }
222
223 bool AbstractSqlStorage::watchQuery(QSqlQuery &query) {
224   if(query.lastError().isValid()) {
225     qCritical() << "unhandled Error in QSqlQuery!";
226     qCritical() << "                  last Query:\n" << query.lastQuery();
227     qCritical() << "              executed Query:\n" << query.executedQuery();
228     qCritical() << "                bound Values:";
229     QList<QVariant> list = query.boundValues().values();
230     for (int i = 0; i < list.size(); ++i)
231       qCritical() << i << ": " << list.at(i).toString().toAscii().data();
232     qCritical() << "                Error Number:"   << query.lastError().number();
233     qCritical() << "               Error Message:"   << query.lastError().text();
234     qCritical() << "              Driver Message:"   << query.lastError().driverText();
235     qCritical() << "                  DB Message:"   << query.lastError().databaseText();
236
237     return false;
238   }
239   return true;
240 }
241
242 void AbstractSqlStorage::connectionDestroyed() {
243   QMutexLocker locker(&_connectionPoolMutex);
244   _connectionPool.remove(sender()->thread());
245 }
246
247 // ========================================
248 //  AbstractSqlStorage::Connection
249 // ========================================
250 AbstractSqlStorage::Connection::Connection(const QString &name, QObject *parent)
251   : QObject(parent),
252     _name(name.toLatin1())
253 {
254 }
255
256 AbstractSqlStorage::Connection::~Connection() {
257   {
258     QSqlDatabase db = QSqlDatabase::database(name(), false);
259     if(db.isOpen()) {
260       db.commit();
261       db.close();
262     }
263   }
264   QSqlDatabase::removeDatabase(name());
265 }
266
267
268
269
270 // ========================================
271 //  AbstractSqlMigrator
272 // ========================================
273 AbstractSqlMigrator::AbstractSqlMigrator()
274   : _query(0)
275 {
276 }
277
278 void AbstractSqlMigrator::newQuery(const QString &query, QSqlDatabase db) {
279   Q_ASSERT(!_query);
280   _query = new QSqlQuery(db);
281   _query->prepare(query);
282 }
283
284 void AbstractSqlMigrator::resetQuery() {
285   delete _query;
286   _query = 0;
287 }
288
289 bool AbstractSqlMigrator::exec() {
290   Q_ASSERT(_query);
291   _query->exec();
292   return !_query->lastError().isValid();
293 }
294
295 QString AbstractSqlMigrator::migrationObject(MigrationObject moType) {
296   switch(moType) {
297   case QuasselUser:
298     return "QuasselUser";
299   case Sender:
300     return "Sender";
301   case Identity:
302     return "Identity";
303   case IdentityNick:
304     return "IdentityNick";
305   case Network:
306     return "Network";
307   case Buffer:
308     return "Buffer";
309   case Backlog:
310     return "Backlog";
311   case IrcServer:
312     return "IrcServer";
313   case UserSetting:
314     return "UserSetting";
315   };
316   return QString();
317 }
318
319 QVariantList AbstractSqlMigrator::boundValues() {
320   QVariantList values;
321   if(!_query)
322     return values;
323
324   int numValues = _query->boundValues().count();
325   for(int i = 0; i < numValues; i++) {
326     values << _query->boundValue(i);
327   }
328   return values;
329 }
330
331 void AbstractSqlMigrator::dumpStatus() {
332   qWarning() << "  executed Query:";
333   qWarning() << qPrintable(executedQuery());
334   qWarning() << "  bound Values:";
335   QList<QVariant> list = boundValues();
336   for (int i = 0; i < list.size(); ++i)
337     qWarning() << i << ": " << list.at(i).toString().toAscii().data();
338   qWarning() << "  Error Number:"   << lastError().number();
339   qWarning() << "  Error Message:"   << lastError().text();
340 }
341
342
343 // ========================================
344 //  AbstractSqlMigrationReader
345 // ========================================
346 AbstractSqlMigrationReader::AbstractSqlMigrationReader()
347   : AbstractSqlMigrator(),
348     _writer(0)
349 {
350 }
351
352 bool AbstractSqlMigrationReader::migrateTo(AbstractSqlMigrationWriter *writer) {
353   if(!transaction()) {
354     qWarning() << "AbstractSqlMigrationReader::migrateTo(): unable to start reader's transaction!";
355     return false;
356   }
357   if(!writer->transaction()) {
358     qWarning() << "AbstractSqlMigrationReader::migrateTo(): unable to start writer's transaction!";
359     rollback(); // close the reader transaction;
360     return false;
361   }
362
363   _writer = writer;
364
365   // due to the incompatibility across Migration objects we can't run this in a loop... :/
366   QuasselUserMO quasselUserMo;
367   if(!transferMo(QuasselUser, quasselUserMo))
368      return false;
369
370   IdentityMO identityMo;
371   if(!transferMo(Identity, identityMo))
372     return false;
373
374   IdentityNickMO identityNickMo;
375   if(!transferMo(IdentityNick, identityNickMo))
376     return false;
377
378   NetworkMO networkMo;
379   if(!transferMo(Network, networkMo))
380     return false;
381
382   BufferMO bufferMo;
383   if(!transferMo(Buffer, bufferMo))
384     return false;
385
386   SenderMO senderMo;
387   if(!transferMo(Sender, senderMo))
388     return false;
389
390   BacklogMO backlogMo;
391   if(!transferMo(Backlog, backlogMo))
392     return false;
393
394   IrcServerMO ircServerMo;
395   if(!transferMo(IrcServer, ircServerMo))
396     return false;
397
398   UserSettingMO userSettingMo;
399   if(!transferMo(UserSetting, userSettingMo))
400     return false;
401
402   if(!_writer->postProcess())
403     abortMigration();
404   return finalizeMigration();
405 }
406
407 void AbstractSqlMigrationReader::abortMigration(const QString &errorMsg) {
408   qWarning() << "Migration Failed!";
409   if(!errorMsg.isNull()) {
410     qWarning() << qPrintable(errorMsg);
411   }
412   if(lastError().isValid()) {
413     qWarning() << "ReaderError:";
414     dumpStatus();
415   }
416
417
418   if(_writer->lastError().isValid()) {
419     qWarning() << "WriterError:";
420     _writer->dumpStatus();
421   }
422
423   rollback();
424   _writer->rollback();
425   _writer = 0;
426 }
427
428 bool AbstractSqlMigrationReader::finalizeMigration() {
429   resetQuery();
430   _writer->resetQuery();
431
432   commit();
433   if(!_writer->commit()) {
434     _writer = 0;
435     return false;
436   }
437   _writer = 0;
438   return true;
439 }
440
441 template<typename T>
442 bool AbstractSqlMigrationReader::transferMo(MigrationObject moType, T &mo) {
443   resetQuery();
444   _writer->resetQuery();
445
446   if(!prepareQuery(moType)) {
447     abortMigration(QString("AbstractSqlMigrationReader::migrateTo(): unable to prepare reader query of type %1!").arg(AbstractSqlMigrator::migrationObject(moType)));
448     return false;
449   }
450   if(!_writer->prepareQuery(moType)) {
451     abortMigration(QString("AbstractSqlMigrationReader::migrateTo(): unable to prepare writer query of type %1!").arg(AbstractSqlMigrator::migrationObject(moType)));
452     return false;
453   }
454
455   qDebug() << qPrintable(QString("Transferring %1...").arg(AbstractSqlMigrator::migrationObject(moType)));
456   int i = 0;
457   QFile file;
458   file.open(stdout, QIODevice::WriteOnly);
459
460   while(readMo(mo)) {
461     if(!_writer->writeMo(mo)) {
462       abortMigration(QString("AbstractSqlMigrationReader::transferMo(): unable to transfer Migratable Object of type %1!").arg(AbstractSqlMigrator::migrationObject(moType)));
463       rollback();
464       _writer->rollback();
465       return false;
466     }
467     i++;
468     if(i % 1000 == 0) {
469       file.write("*");
470       file.flush();
471     }
472   }
473   if(i > 1000) {
474     file.write("\n");
475     file.flush();
476   }
477
478   qDebug() << "Done.";
479   return true;
480 }
481