1 /***************************************************************************
2 * Copyright (C) 2005-2009 by Rajko Albrecht ral@alwins-world.de *
3 * http://kdesvn.alwins-world.de/ *
4 * *
5 * This program is free software; you can redistribute it and/or *
6 * modify it under the terms of the GNU Lesser General Public *
7 * License as published by the Free Software Foundation; either *
8 * version 2.1 of the License, or (at your option) any later version. *
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 GNU *
13 * Lesser General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU Lesser General Public *
16 * License along with this program (in the file LGPL.txt); if not, *
17 * write to the Free Software Foundation, Inc., 51 Franklin St, *
18 * Fifth Floor, Boston, MA 02110-1301 USA *
19 * *
20 * This software consists of voluntary contributions made by many *
21 * individuals. For exact contribution history, see the revision *
22 * history and logs, available at http://kdesvn.alwins-world.de. *
23 ***************************************************************************/
24 #include "LogCache.h"
25
26 #include <QDebug>
27 #include <QDir>
28 #include <QMap>
29 #include <QMutex>
30 #include <QThreadStorage>
31 #include <QSqlDatabase>
32 #include <QSqlError>
33 #include <QSqlQuery>
34 #include <QVariant>
35
36 #include "svnqt/path.h"
37 #include "svnqt/cache/DatabaseException.h"
38
SQLTYPE()39 static QString SQLTYPE() { return QStringLiteral("QSQLITE"); }
SQLMAIN()40 static QString SQLMAIN() { return QStringLiteral("logmain-logcache"); }
SQLMAINTABLE()41 static QString SQLMAINTABLE() { return QStringLiteral("logdb"); }
SQLTMPDB()42 static QString SQLTMPDB() { return QStringLiteral("tmpdb"); }
SQLREPOSPARAMETER()43 static QString SQLREPOSPARAMETER() { return QStringLiteral("repoparameter"); }
SQLSTATUS()44 static QString SQLSTATUS() { return QStringLiteral("logstatus"); }
45
46 namespace svn
47 {
48 namespace cache
49 {
50
51 LogCache *LogCache::mSelf = nullptr;
52
53 class ThreadDBStore
54 {
55 public:
ThreadDBStore()56 ThreadDBStore()
57 {
58 m_DB = QSqlDatabase();
59 }
~ThreadDBStore()60 ~ThreadDBStore()
61 {
62 m_DB.commit();
63 m_DB.close();
64 m_DB = QSqlDatabase();
65 QMap<QString, QString>::Iterator it;
66 for (it = reposCacheNames.begin(); it != reposCacheNames.end(); ++it) {
67 if (QSqlDatabase::database(it.value()).isOpen()) {
68 QSqlDatabase::database(it.value()).commit();
69 QSqlDatabase::database(it.value()).close();
70 }
71 QSqlDatabase::removeDatabase(it.value());
72 }
73 QSqlDatabase::removeDatabase(key);
74 }
75
deleteDb(const QString & path)76 void deleteDb(const QString &path)
77 {
78 QMap<QString, QString>::Iterator it;
79 for (it = reposCacheNames.begin(); it != reposCacheNames.end(); ++it) {
80 QSqlDatabase _db = QSqlDatabase::database(it.value());
81 if (_db.databaseName() == path) {
82 qDebug() << "Removing database " << _db.databaseName() << endl;
83 if (_db.isOpen()) {
84 _db.commit();
85 _db.close();
86 }
87 QSqlDatabase::removeDatabase(it.value());
88 it = reposCacheNames.begin();
89 }
90 }
91 }
92 QSqlDatabase m_DB;
93 QString key;
94 QMap<QString, QString> reposCacheNames;
95 };
96
97 class LogCacheData
98 {
99
100 protected:
101 QMutex m_singleDbMutex;
102
103 public:
LogCacheData()104 LogCacheData() {}
~LogCacheData()105 ~LogCacheData()
106 {
107 if (m_mainDB.hasLocalData()) {
108 m_mainDB.localData()->m_DB.close();
109 m_mainDB.setLocalData(0L);
110 }
111 }
112
idToPath(const QString & id) const113 QString idToPath(const QString &id) const
114 {
115 return m_BasePath + QLatin1Char('/') + id + QLatin1String(".db");
116 }
117
deleteRepository(const QString & aRepository)118 bool deleteRepository(const QString &aRepository)
119 {
120 const QString id = getReposId(aRepository);
121
122 static const QString s_q(QLatin1String("delete from ") + SQLREPOSPARAMETER() + QLatin1String(" where id = ?"));
123 static const QString r_q(QLatin1String("delete from ") + SQLMAINTABLE() + QLatin1String(" where id = ?"));
124 QSqlDatabase mainDB = getMainDB();
125 if (!mainDB.isValid()) {
126 qWarning("Failed to open main database.");
127 return false;
128 }
129 qDebug() << m_mainDB.localData()->reposCacheNames;
130 m_mainDB.localData()->deleteDb(idToPath(id));
131 qDebug() << m_mainDB.localData()->reposCacheNames;
132 QFile fi(idToPath(id));
133 if (fi.exists()) {
134 if (!fi.remove()) {
135 qWarning() << "Could not delete " << fi.fileName();
136 return false;
137 }
138 }
139 qDebug() << "Removed " << fi.fileName() << endl;
140 mainDB.transaction();
141 QSqlQuery _q(mainDB);
142 _q.prepare(s_q);
143 _q.bindValue(0, id);
144 if (!_q.exec()) {
145 qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
146 _q.finish();
147 mainDB.rollback();
148 return false;
149 }
150 _q.prepare(r_q);
151 _q.bindValue(0, id);
152 if (!_q.exec()) {
153 qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
154 _q.finish();
155 mainDB.rollback();
156 return false;
157 }
158 mainDB.commit();
159 return true;
160 }
161
checkReposDb(QSqlDatabase aDb)162 bool checkReposDb(QSqlDatabase aDb)
163 {
164 if (!aDb.open()) {
165 return false;
166 }
167
168 QSqlQuery _q(aDb);
169 QStringList list = aDb.tables();
170
171 aDb.transaction();
172 if (!list.contains(QStringLiteral("logentries"))) {
173 _q.exec(QStringLiteral("CREATE TABLE \"logentries\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER UNIQUE,\"date\" INTEGER,\"author\" TEXT, \"message\" TEXT)"));
174 }
175 if (!list.contains(QStringLiteral("changeditems"))) {
176 _q.exec(QStringLiteral("CREATE TABLE \"changeditems\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER,\"changeditem\" TEXT,\"action\" TEXT,\"copyfrom\" TEXT,\"copyfromrev\" INTEGER, UNIQUE(revision,changeditem,action))"));
177 }
178 if (!list.contains(QStringLiteral("mergeditems"))) {
179 _q.exec(QStringLiteral("CREATE TABLE \"mergeditems\" (\"revision\" INTEGER,\"mergeditems\" TEXT, PRIMARY KEY(revision))"));
180 }
181 if (!list.contains(QStringLiteral("dbversion"))) {
182 _q.exec(QStringLiteral("CREATE TABLE \"dbversion\" (\"version\" INTEGER)"));
183 qDebug() << _q.lastError();
184 _q.exec(QStringLiteral("INSERT INTO \"dbversion\" (version) VALUES(0)"));
185 }
186 aDb.commit();
187 list = aDb.tables();
188 if (!list.contains(QStringLiteral("logentries")) ||
189 !list.contains(QStringLiteral("changeditems")) ||
190 !list.contains(QStringLiteral("mergeditems")) ||
191 !list.contains(QStringLiteral("dbversion"))) {
192 qDebug() << "lists: " << list;
193 return false;
194 }
195 _q.exec(QStringLiteral("SELECT VERSION from dbversion limit 1"));
196 if (_q.lastError().type() == QSqlError::NoError && _q.next()) {
197 int version = _q.value(0).toInt();
198 if (version == 0) {
199 _q.exec(QStringLiteral("create index if not exists main.authorindex on logentries(author)"));
200 if (_q.lastError().type() != QSqlError::NoError) {
201 qDebug() << _q.lastError();
202 } else {
203 _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=1"));
204 }
205 ++version;
206 }
207 if (version == 1) {
208 _q.exec(QStringLiteral("create index if not exists main.dateindex on logentries(date)"));
209 if (_q.lastError().type() != QSqlError::NoError) {
210 qDebug() << _q.lastError();
211 } else {
212 _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=2"));
213 }
214 ++version;
215 }
216 } else {
217 qDebug() << "Select: " << _q.lastError();
218 }
219 return true;
220 }
221
createReposDB(const svn::Path & reposroot)222 QString createReposDB(const svn::Path &reposroot)
223 {
224 QMutexLocker locker(&m_singleDbMutex);
225
226 QSqlDatabase _mdb = getMainDB();
227
228 _mdb.transaction();
229 QSqlQuery query(_mdb);
230 QString q(QLatin1String("insert into ") + SQLMAINTABLE() + QLatin1String(" (reposroot) VALUES('") + reposroot.path() + QLatin1String("')"));
231
232 if (!query.exec(q)) {
233 return QString();
234 }
235
236 _mdb.commit();
237 query.prepare(reposSelect());
238 query.bindValue(0, reposroot.native());
239 QString db;
240 if (query.exec() && query.next()) {
241 db = query.value(0).toString();
242 } else {
243 //qDebug() << "Error select_01: " << query.lastError().text() << "(" << query.lastQuery() << ")";
244 }
245 if (!db.isEmpty()) {
246 QString fulldb = idToPath(db);
247 QSqlDatabase _db = QSqlDatabase::addDatabase(SQLTYPE(), SQLTMPDB());
248 _db.setDatabaseName(fulldb);
249 if (!checkReposDb(_db)) {
250 }
251 QSqlDatabase::removeDatabase(SQLTMPDB());
252 }
253 return db;
254 }
255
getReposId(const svn::Path & reposroot)256 QString getReposId(const svn::Path &reposroot)
257 {
258 if (!getMainDB().isValid()) {
259 return QString();
260 }
261 QSqlQuery c(getMainDB());
262 c.prepare(reposSelect());
263 c.bindValue(0, reposroot.native());
264
265 // only the first one
266 if (c.exec() && c.next()) {
267 return c.value(0).toString();
268 }
269 return QString();
270 }
271
getReposDB(const svn::Path & reposroot)272 QSqlDatabase getReposDB(const svn::Path &reposroot)
273 {
274 if (!getMainDB().isValid()) {
275 return QSqlDatabase();
276 }
277 QString dbFile = getReposId(reposroot);
278
279 if (dbFile.isEmpty()) {
280 dbFile = createReposDB(reposroot);
281 if (dbFile.isEmpty()) {
282 return QSqlDatabase();
283 }
284 }
285 if (m_mainDB.localData()->reposCacheNames.find(dbFile) != m_mainDB.localData()->reposCacheNames.end()) {
286 QSqlDatabase db = QSqlDatabase::database(m_mainDB.localData()->reposCacheNames.value(dbFile));
287 checkReposDb(db);
288 return db;
289 }
290 unsigned i = 0;
291 QString _key = dbFile;
292 while (QSqlDatabase::contains(_key)) {
293 _key = QStringLiteral("%1-%2").arg(dbFile).arg(i++);
294 }
295 const QString fulldb = idToPath(dbFile);
296 QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
297 db.setDatabaseName(fulldb);
298 if (!checkReposDb(db)) {
299 db = QSqlDatabase();
300 } else {
301 m_mainDB.localData()->reposCacheNames[dbFile] = _key;
302 }
303 return db;
304 }
305
getMainDB() const306 QSqlDatabase getMainDB()const
307 {
308 if (!m_mainDB.hasLocalData()) {
309 unsigned i = 0;
310 QString _key = SQLMAIN();
311 while (QSqlDatabase::contains(_key)) {
312 _key = QStringLiteral("%1-%2").arg(SQLMAIN()).arg(i++);
313 }
314 QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
315 db.setDatabaseName(m_BasePath + QLatin1String("/maindb.db"));
316 if (db.open()) {
317 m_mainDB.setLocalData(new ThreadDBStore);
318 m_mainDB.localData()->key = _key;
319 m_mainDB.localData()->m_DB = db;
320 }
321 }
322 if (m_mainDB.hasLocalData()) {
323 return m_mainDB.localData()->m_DB;
324 } else {
325 return QSqlDatabase();
326 }
327 }
328 QString m_BasePath;
329
330 mutable QThreadStorage<ThreadDBStore *> m_mainDB;
331
reposSelect()332 static QString reposSelect()
333 {
334 return QStringLiteral("SELECT id from ") +
335 SQLMAINTABLE() +
336 QStringLiteral(" where reposroot=? ORDER by id DESC");
337 }
338 };
339
340 /*!
341 \fn svn::cache::LogCache::LogCache()
342 */
LogCache()343 LogCache::LogCache()
344 : m_BasePath(QDir::homePath() + QLatin1String("/.svnqt"))
345 {
346 setupCachePath();
347 }
348
LogCache(const QString & aBasePath)349 LogCache::LogCache(const QString &aBasePath)
350 {
351 delete mSelf;
352 mSelf = this;
353 if (aBasePath.isEmpty()) {
354 m_BasePath = QDir::homePath() + QLatin1String("/.svnqt");
355 } else {
356 m_BasePath = aBasePath;
357 }
358 setupCachePath();
359 }
360
~LogCache()361 LogCache::~LogCache()
362 {
363 }
364
365 /*!
366 \fn svn::cache::LogCache::setupCachePath()
367 */
setupCachePath()368 void LogCache::setupCachePath()
369 {
370 m_CacheData.reset(new LogCacheData);
371 m_CacheData->m_BasePath = m_BasePath;
372 QDir d;
373 if (!d.exists(m_BasePath)) {
374 d.mkdir(m_BasePath);
375 }
376 m_BasePath = m_BasePath + QLatin1Char('/') + QLatin1String("logcache");
377 if (!d.exists(m_BasePath)) {
378 d.mkdir(m_BasePath);
379 }
380 m_CacheData->m_BasePath = m_BasePath;
381 if (d.exists(m_BasePath)) {
382 setupMainDb();
383 }
384 }
385
setupMainDb()386 void LogCache::setupMainDb()
387 {
388 QSqlDatabase mainDB = m_CacheData->getMainDB();
389 if (!mainDB.isValid()) {
390 qWarning("Failed to open main database.");
391 } else {
392 const QStringList list = mainDB.tables();
393 QSqlQuery q(mainDB);
394 if (!list.contains(SQLSTATUS())) {
395 mainDB.transaction();
396 if (q.exec(QLatin1String("CREATE TABLE \"") + SQLSTATUS() + QLatin1String("\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"value\" TEXT);"))) {
397 q.exec(QLatin1String("INSERT INTO \"") + SQLSTATUS() + QLatin1String("\" (key,value) values(\"version\",\"0\");"));
398 }
399 mainDB.commit();
400 }
401 int version = databaseVersion();
402 if (version == 0) {
403 mainDB.transaction();
404 if (!list.contains(SQLMAINTABLE())) {
405 q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLMAINTABLE() + QLatin1String("\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);"));
406 }/* else {
407 q.exec("CREATE TABLE IF NOT EXISTS \""+QString(SQLMAINTABLE)+"new\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);");
408 q.exec("insert into \""+QString(SQLMAINTABLE)+"new\" select \"reposroot\",\"id\" from \""+QString(SQLMAINTABLE)+"\");");
409 q.exec("drop table \""+QString(SQLMAINTABLE)+"\";");
410 q.exec("alter table \""+QString(SQLMAINTABLE)+"new\" to \""+QString(SQLMAINTABLE)+"\";");
411 }*/
412 ++version;
413 }
414 if (version == 1) {
415 mainDB.transaction();
416 if (!q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLREPOSPARAMETER() +
417 QLatin1String("\" (\"id\" INTEGER NOT NULL, \"parameter\" TEXT, \"value\" TEXT, PRIMARY KEY(\"id\",\"parameter\"));"))) {
418 qDebug() << "Error create: " << q.lastError().text() << "(" << q.lastQuery() << ")";
419 }
420 mainDB.commit();
421 ++version;
422 }
423 databaseVersion(version);
424 }
425 }
426
databaseVersion(int newversion)427 void LogCache::databaseVersion(int newversion)
428 {
429 QSqlDatabase mainDB = m_CacheData->getMainDB();
430 if (!mainDB.isValid()) {
431 return;
432 }
433 static const QString _qs(QLatin1String("update \"") + SQLSTATUS() + QLatin1String("\" SET value = ? WHERE \"key\" = \"version\""));
434 QSqlQuery cur(mainDB);
435 cur.prepare(_qs);
436 cur.bindValue(0, newversion);
437 if (!cur.exec()) {
438 qDebug() << "Error set version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
439 }
440 }
441
databaseVersion() const442 int LogCache::databaseVersion()const
443 {
444 QSqlDatabase mainDB = m_CacheData->getMainDB();
445 if (!mainDB.isValid()) {
446 return -1;
447 }
448 static const QString _qs(QLatin1String("select value from \"") + SQLSTATUS() + QLatin1String("\" WHERE \"key\" = \"version\""));
449 QSqlQuery cur(mainDB);
450 cur.prepare(_qs);
451 if (!cur.exec()) {
452 qDebug() << "Error select version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
453 return -1;
454 }
455 if (cur.isActive() && cur.next()) {
456 //qDebug("Sel result: %s",_q.value(0).toString().toUtf8().data());
457 return cur.value(0).toInt();
458 }
459 return -1;
460 }
461
getRepositoryParameter(const svn::Path & repository,const QString & key) const462 QVariant LogCache::getRepositoryParameter(const svn::Path &repository, const QString &key)const
463 {
464 QSqlDatabase mainDB = m_CacheData->getMainDB();
465 if (!mainDB.isValid()) {
466 return QVariant();
467 }
468 static const QString qs(QLatin1String("select \"value\",\"repoparameter\".\"parameter\" as \"key\" from \"") + SQLREPOSPARAMETER() +
469 QLatin1String("\" INNER JOIN \"") + SQLMAINTABLE() + QLatin1String("\" ON (\"") + SQLREPOSPARAMETER() +
470 QLatin1String("\".id = \"") + SQLMAINTABLE() + QLatin1String("\".id and \"") + SQLMAINTABLE() +
471 QLatin1String("\".reposroot = ?) WHERE \"parameter\" = ?;"));
472 QSqlQuery cur(mainDB);
473 cur.prepare(qs);
474 cur.bindValue(0, repository.native());
475 cur.bindValue(1, key);
476 if (!cur.exec()) {
477 qWarning() << "Error select: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
478 return QVariant();
479 }
480 if (cur.isActive() && cur.next()) {
481 return cur.value(0);
482 }
483 return QVariant();
484 }
485
setRepositoryParameter(const svn::Path & repository,const QString & key,const QVariant & value)486 bool LogCache::setRepositoryParameter(const svn::Path &repository, const QString &key, const QVariant &value)
487 {
488 QSqlDatabase mainDB = m_CacheData->getMainDB();
489 if (!mainDB.isValid()) {
490 return false;
491 }
492 QString id = m_CacheData->getReposId(repository);
493 if (id.isEmpty()) {
494 return false;
495 }
496 static const QString qs(QLatin1String("INSERT OR REPLACE INTO \"") + SQLREPOSPARAMETER() +
497 QLatin1String("\" (\"id\",\"parameter\",\"value\") values (\"%1\",\"%2\",?);"));
498 static const QString dqs(QLatin1String("DELETE FROM \"") + SQLREPOSPARAMETER() +
499 QLatin1String("\" WHERE \"id\"=? and \"parameter\" = ?"));
500 mainDB.transaction();
501 QSqlQuery cur(mainDB);
502 if (value.isValid()) {
503 QString _qs = qs.arg(id,key);//.arg(value.toByteArray());
504 cur.prepare(_qs);
505 cur.bindValue(0, value);
506 if (!cur.exec()) {
507 qDebug() << "Error insert new value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
508 cur.finish();
509 mainDB.rollback();
510 return false;
511 }
512 } else {
513 cur.prepare(dqs);
514 cur.bindValue(0, id);
515 cur.bindValue(1, key);
516 if (!cur.exec()) {
517 qDebug() << "Error delete value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
518 cur.finish();
519 mainDB.rollback();
520 return false;
521 }
522 }
523 mainDB.commit();
524 return true;
525 }
526
527 }
528 }
529
530 /*!
531 \fn svn::cache::LogCache::self()
532 */
self()533 svn::cache::LogCache *svn::cache::LogCache::self()
534 {
535 if (!mSelf) {
536 mSelf = new LogCache();
537 }
538 return mSelf;
539 }
540
541 /*!
542 \fn svn::cache::LogCache::reposDb()
543 */
reposDb(const QString & aRepository)544 QSqlDatabase svn::cache::LogCache::reposDb(const QString &aRepository)
545 {
546 // //qDebug("reposDB");
547 return m_CacheData->getReposDB(aRepository);
548 }
549
550 /*!
551 \fn svn::cache::LogCache::cachedRepositories()const
552 */
cachedRepositories() const553 QStringList svn::cache::LogCache::cachedRepositories()const
554 {
555 static const QString s_q(QLatin1String("select \"reposroot\" from ") + SQLMAINTABLE() + QLatin1String(" order by reposroot"));
556 QSqlDatabase mainDB = m_CacheData->getMainDB();
557 QStringList _res;
558 if (!mainDB.isValid()) {
559 qWarning("Failed to open main database.");
560 return _res;
561 }
562 QSqlQuery cur(mainDB);
563 cur.prepare(s_q);
564 if (!cur.exec()) {
565 throw svn::cache::DatabaseException(QLatin1String("Could not retrieve values: ") + cur.lastError().text());
566 return _res;
567 }
568 while (cur.next()) {
569 _res.append(cur.value(0).toString());
570 }
571
572 return _res;
573 }
574
valid() const575 bool svn::cache::LogCache::valid()const
576 {
577 return m_CacheData->getMainDB().isValid();
578 }
579
deleteRepository(const QString & aRepository)580 bool svn::cache::LogCache::deleteRepository(const QString &aRepository)
581 {
582 return m_CacheData->deleteRepository(aRepository);
583 }
584