Akonadi

dbmigrator.cpp
1/*
2 SPDX-FileCopyrightText: 2024 g10 Code GmbH
3 SPDX-FileContributor: Daniel Vrátil <dvratil@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "dbmigrator.h"
9#include "ControlManager.h"
10#include "akonadidbmigrator_debug.h"
11#include "akonadifull-version.h"
12#include "akonadischema.h"
13#include "akranges.h"
14#include "entities.h"
15#include "private/dbus_p.h"
16#include "private/standarddirs_p.h"
17#include "storage/countquerybuilder.h"
18#include "storage/datastore.h"
19#include "storage/dbconfig.h"
20#include "storage/dbconfigmysql.h"
21#include "storage/dbconfigpostgresql.h"
22#include "storage/dbconfigsqlite.h"
23#include "storage/dbintrospector.h"
24#include "storage/querybuilder.h"
25#include "storage/schematypes.h"
26#include "storage/transaction.h"
27#include "storagejanitor.h"
28#include "utils.h"
29
30#include <QCommandLineOption>
31#include <QCommandLineParser>
32#include <QCoreApplication>
33#include <QDBusConnection>
34#include <QDBusError>
35#include <QDBusInterface>
36#include <QDir>
37#include <QElapsedTimer>
38#include <QFile>
39#include <QFileInfo>
40#include <QScopeGuard>
41#include <QSettings>
42#include <QSqlDatabase>
43#include <QSqlError>
44#include <QSqlQuery>
45#include <QSqlRecord>
46#include <QThread>
47
48#include <KLocalizedString>
49
50#include <chrono>
51
52using namespace Akonadi;
53using namespace Akonadi::Server;
54using namespace AkRanges;
55using namespace std::chrono_literals;
56
57Q_DECLARE_METATYPE(UIDelegate::Result);
58
59namespace
60{
61
62constexpr size_t maxTransactionSize = 1000; // Arbitary guess
63
64class MigratorDataStoreFactory : public DataStoreFactory
65{
66public:
67 explicit MigratorDataStoreFactory(DbConfig *config)
68 : m_dbConfig(config)
69 {
70 }
71
72 DataStore *createStore() override
73 {
74 class MigratorDataStore : public DataStore
75 {
76 public:
77 explicit MigratorDataStore(DbConfig *config)
78 : DataStore(config)
79 {
80 }
81 };
82 return new MigratorDataStore(m_dbConfig);
83 }
84
85private:
86 DbConfig *const m_dbConfig;
87};
88
89class Rollback
90{
91 Q_DISABLE_COPY_MOVE(Rollback)
92public:
93 Rollback() = default;
94 ~Rollback()
95 {
96 run();
97 }
98
99 void reset()
100 {
101 mRollbacks.clear();
102 }
103
104 void run()
105 {
106 // Run rollbacks in reverse order!
107 for (auto it = mRollbacks.rbegin(); it != mRollbacks.rend(); ++it) {
108 (*it)();
109 }
110 mRollbacks.clear();
111 }
112
113 template<typename T>
114 void add(T &&rollback)
115 {
116 mRollbacks.push_back(std::forward<T>(rollback));
117 }
118
119private:
120 std::vector<std::function<void()>> mRollbacks;
121};
122
123void stopDatabaseServer(DbConfig *config)
124{
125 config->stopInternalServer();
126}
127
128bool createDatabase(DbConfig *config)
129{
130 auto db = QSqlDatabase::addDatabase(config->driverName(), QStringLiteral("initConnection"));
131 config->apply(db);
132 db.setDatabaseName(config->databaseName());
133 if (!db.isValid()) {
134 qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid database object during initial database connection";
135 return false;
136 }
137
138 const auto closeDb = qScopeGuard([&db]() {
139 db.close();
140 });
141
142 // Try to connect to the database and select the Akonadi DB
143 if (db.open()) {
144 return true;
145 }
146
147 // That failed - the database might not exist yet..
148 qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to use database" << config->databaseName();
149 qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
150 qCDebug(AKONADIDBMIGRATOR_LOG) << "Trying to create database now...";
151
152 db.close();
153 db.setDatabaseName(QString());
154 // Try to just connect to the DB server without selecting a database
155 if (!db.open()) {
156 // Failed, DB server not running or broken
157 qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to connect to database!";
158 qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
159 return false;
160 }
161
162 // Try to create the database
163 QSqlQuery query(db);
164 if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(config->databaseName()))) {
165 qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to create database";
166 qCCritical(AKONADIDBMIGRATOR_LOG) << "Query error:" << query.lastError().text();
167 qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
168 return false;
169 }
170 return true;
171}
172
173std::unique_ptr<DataStore> prepareDatabase(DbConfig *config)
174{
175 if (config->useInternalServer()) {
176 if (!config->startInternalServer()) {
177 qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to start the database server";
178 return {};
179 }
180 } else {
181 if (!createDatabase(config)) {
182 return {};
183 }
184 }
185
186 config->setup();
187
188 auto factory = std::make_unique<MigratorDataStoreFactory>(config);
189 std::unique_ptr<DataStore> store{factory->createStore()};
190 if (!store->database().isOpen()) {
191 return {};
192 }
193 if (!store->init()) {
194 return {};
195 }
196
197 return store;
198}
199
200void cleanupDatabase(DataStore *store, DbConfig *config)
201{
202 store->close();
203
204 stopDatabaseServer(config);
205}
206
207bool syncAutoIncrementValue(DataStore *sourceStore, DataStore *destStore, const TableDescription &table)
208{
209 const auto idCol = std::find_if(table.columns.begin(), table.columns.end(), [](const auto &col) {
210 return col.isPrimaryKey;
211 });
212 if (idCol == table.columns.end()) {
213 return false;
214 }
215
216 const auto getAutoIncrementValue = [](DataStore *store, const QString &table, const QString &idCol) -> std::optional<qint64> {
217 const auto db = store->database();
218 const auto introspector = DbIntrospector::createInstance(db);
219
220 QSqlQuery query(db);
221 if (!query.exec(introspector->getAutoIncrementValueQuery(table, idCol))) {
222 qCCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
223 return {};
224 }
225 if (!query.next()) {
226 // SQLite returns an empty result set for empty tables, so we assume the table is empty and
227 // the counter starts at 1
228 return 1;
229 }
230
231 return query.value(0).toLongLong();
232 };
233
234 const auto setAutoIncrementValue = [](DataStore *store, const QString &table, const QString &idCol, qint64 seq) -> bool {
235 const auto db = store->database();
236 const auto introspector = DbIntrospector::createInstance(db);
237
238 QSqlQuery query(db);
239 if (!query.exec(introspector->updateAutoIncrementValueQuery(table, idCol, seq))) {
240 qCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
241 return false;
242 }
243
244 return true;
245 };
246
247 const auto seq = getAutoIncrementValue(sourceStore, table.name, idCol->name);
248 if (!seq) {
249 return false;
250 }
251
252 return setAutoIncrementValue(destStore, table.name, idCol->name, *seq);
253}
254
255bool analyzeTable(const QString &table, DataStore *store)
256{
257 auto dbType = DbType::type(store->database());
258 QString queryStr;
259 switch (dbType) {
260 case DbType::Sqlite:
261 case DbType::PostgreSQL:
262 queryStr = QLatin1StringView("ANALYZE ") + table;
263 break;
264 case DbType::MySQL:
265 queryStr = QLatin1StringView("ANALYZE TABLE ") + table;
266 break;
267 case DbType::Unknown:
268 qCCritical(AKONADIDBMIGRATOR_LOG) << "Unknown database type";
269 return false;
270 }
271
272 QSqlQuery query(store->database());
273 if (!query.exec(queryStr)) {
274 qCCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
275 return false;
276 }
277
278 return true;
279}
280
281QString createTmpAkonadiServerRc(const QString &targetEngine)
282{
283 const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
284 const auto newFileName = QStringLiteral("%1.new").arg(origFileName);
285
286 QSettings settings(newFileName, QSettings::IniFormat);
287 settings.setValue(QStringLiteral("General/Driver"), targetEngine);
288
289 return newFileName;
290}
291
292QString driverFromEngineName(const QString &engine)
293{
294 const auto enginelc = engine.toLower();
295 if (enginelc == QLatin1StringView("sqlite")) {
296 return QStringLiteral("QSQLITE");
297 }
298 if (enginelc == QLatin1StringView("mysql")) {
299 return QStringLiteral("QMYSQL");
300 }
301 if (enginelc == QLatin1StringView("postgres")) {
302 return QStringLiteral("QPSQL");
303 }
304
305 qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid engine:" << engine;
306 return {};
307}
308
309std::unique_ptr<DbConfig> dbConfigFromServerRc(const QString &configFile, bool overrideDbPath = false)
310{
311 QSettings settings(configFile, QSettings::IniFormat);
312 const auto driver = settings.value(QStringLiteral("General/Driver")).toString();
313 std::unique_ptr<DbConfig> config;
314 QString dbPathSuffix;
315 if (driver == QLatin1StringView("QSQLITE") || driver == QLatin1StringView("QSQLITE3")) {
316 config = std::make_unique<DbConfigSqlite>(configFile);
317 } else if (driver == QLatin1StringView("QMYSQL")) {
318 config = std::make_unique<DbConfigMysql>(configFile);
319 dbPathSuffix = QStringLiteral("/db_data");
320 } else if (driver == QLatin1StringView("QPSQL")) {
321 config = std::make_unique<DbConfigPostgresql>(configFile);
322 dbPathSuffix = QStringLiteral("/db_data");
323 } else {
324 qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid driver: " << driver;
325 return {};
326 }
327
328 const auto dbPath = overrideDbPath ? StandardDirs::saveDir("data", QStringLiteral("db_migration%1").arg(dbPathSuffix)) : QString{};
329 config->init(settings, true, dbPath);
330
331 return config;
332}
333
334bool isValidDbPath(const QString &path)
335{
336 if (path.isEmpty()) {
337 return false;
338 }
339
340 const QFileInfo fi(path);
341 return fi.exists() && (fi.isFile() || fi.isDir());
342}
343
344std::error_code restoreDatabaseFromBackup(const QString &backupPath, const QString &originalPath)
345{
346 std::error_code ec;
347 if (QFileInfo::exists(originalPath)) {
348 // Remove the original path, it could have been created by the new db
349 std::filesystem::remove_all(originalPath.toStdString(), ec);
350 return ec;
351 }
352
353 // std::filesystem doesn't care whether it's file or directory, unlike QFile vs QDir.
354 std::filesystem::rename(backupPath.toStdString(), originalPath.toStdString(), ec);
355 return ec;
356}
357
358bool akonadiIsRunning()
359{
360 auto sessionIface = QDBusConnection::sessionBus().interface();
361 return sessionIface->isServiceRegistered(DBus::serviceName(DBus::ControlLock)) || sessionIface->isServiceRegistered(DBus::serviceName(DBus::Server));
362}
363
364bool stopAkonadi()
365{
366 static constexpr auto shutdownTimeout = 5s;
367
368 org::freedesktop::Akonadi::ControlManager manager(DBus::serviceName(DBus::Control), QStringLiteral("/ControlManager"), QDBusConnection::sessionBus());
369 if (!manager.isValid()) {
370 return false;
371 }
372
373 manager.shutdown();
374
375 QElapsedTimer timer;
376 timer.start();
377 while (akonadiIsRunning() && timer.durationElapsed() <= shutdownTimeout) {
378 QThread::msleep(100);
379 }
380
381 return timer.durationElapsed() <= shutdownTimeout && !akonadiIsRunning();
382}
383
384bool startAkonadi()
385{
386 QDBusConnection::sessionBus().interface()->startService(DBus::serviceName(DBus::Control));
387 return true;
388}
389
390bool acquireAkonadiLock()
391{
392 auto connIface = QDBusConnection::sessionBus().interface();
393 auto reply = connIface->registerService(DBus::serviceName(DBus::ControlLock));
394 if (!reply.isValid() || reply != QDBusConnectionInterface::ServiceRegistered) {
395 return false;
396 }
397
398 reply = connIface->registerService(DBus::serviceName(DBus::UpgradeIndicator));
399 if (!reply.isValid() || reply != QDBusConnectionInterface::ServiceRegistered) {
400 return false;
401 }
402
403 return true;
404}
405
406bool releaseAkonadiLock()
407{
408 auto connIface = QDBusConnection::sessionBus().interface();
409 connIface->unregisterService(DBus::serviceName(DBus::ControlLock));
410 connIface->unregisterService(DBus::serviceName(DBus::UpgradeIndicator));
411 return true;
412}
413
414} // namespace
415
416DbMigrator::DbMigrator(const QString &targetEngine, UIDelegate *delegate, QObject *parent)
417 : QObject(parent)
418 , m_targetEngine(targetEngine)
419 , m_uiDelegate(delegate)
420{
421 qRegisterMetaType<UIDelegate::Result>();
422}
423
424DbMigrator::~DbMigrator()
425{
426 if (m_thread) {
427 m_thread->wait();
428 }
429}
430
431void DbMigrator::startMigration()
432{
433 m_thread.reset(QThread::create([this]() {
434 bool restartAkonadi = false;
435 if (akonadiIsRunning()) {
436 emitInfo(i18nc("@info:status", "Stopping Akonadi service..."));
437 restartAkonadi = true;
438 if (!stopAkonadi()) {
439 emitError(i18nc("@info:status", "Error: timeout while waiting for Akonadi to stop."));
440 emitCompletion(false);
441 return;
442 }
443 }
444
445 if (!acquireAkonadiLock()) {
446 emitError(i18nc("@info:status", "Error: couldn't acquire DBus lock for Akonadi."));
447 emitCompletion(false);
448 return;
449 }
450
451 const bool result = runMigrationThread();
452
453 releaseAkonadiLock();
454 if (restartAkonadi) {
455 emitInfo(i18nc("@info:status", "Starting Akonadi service..."));
456 startAkonadi();
457 }
458
459 emitCompletion(result);
460 }));
461 m_thread->start();
462}
463
464bool DbMigrator::runStorageJanitor(DbConfig *dbConfig)
465{
466 StorageJanitor janitor(dbConfig);
467 connect(&janitor, &StorageJanitor::done, this, [this]() {
468 emitInfo(i18nc("@info:status", "Database fsck completed"));
469 });
470 // Runs the janitor in the current thread
471 janitor.check();
472
473 return true;
474}
475
476bool DbMigrator::runMigrationThread()
477{
478 Rollback rollback;
479
480 const auto driver = driverFromEngineName(m_targetEngine);
481 if (driver.isEmpty()) {
482 emitError(i18nc("@info:status", "Invalid database engine \"%1\" - valid values are \"sqlite\", \"mysql\" and \"postgres\".", m_targetEngine));
483 return false;
484 }
485
486 // Create backup akonadiserverrc
487 const auto sourceServerCfgFile = backupAkonadiServerRc();
488 if (!sourceServerCfgFile) {
489 return false;
490 }
491 rollback.add([file = *sourceServerCfgFile]() {
492 QFile::remove(file);
493 });
494
495 // Create new akonadiserverrc with the new engine configuration
496 const QString destServerCfgFile = createTmpAkonadiServerRc(driver);
497 rollback.add([destServerCfgFile]() {
498 QFile::remove(destServerCfgFile);
499 });
500
501 // Create DbConfig for the source DB
502 auto sourceConfig = dbConfigFromServerRc(*sourceServerCfgFile);
503 if (!sourceConfig) {
504 emitError(i18nc("@info:shell", "Error: failed to configure source database server."));
505 return false;
506 }
507
508 if (sourceConfig->driverName() == driver) {
509 emitError(i18nc("@info:shell", "Source and destination database engines are the same."));
510 return false;
511 }
512
513 // Check that we actually have valid source datbase path
514 const auto sourceDbPath = sourceConfig->databasePath();
515 if (!isValidDbPath(sourceDbPath)) {
516 emitError(i18nc("@info:shell", "Error: failed to obtain path to source database data file or directory."));
517 return false;
518 }
519
520 // Configure the new DB server
521 auto destConfig = dbConfigFromServerRc(destServerCfgFile, /* overrideDbPath=*/true);
522 if (!destConfig) {
523 emitError(i18nc("@info:shell", "Error: failed to configure the new database server."));
524 return false;
525 }
526
527 auto sourceStore = prepareDatabase(sourceConfig.get());
528 if (!sourceStore) {
529 emitError(i18nc("@info:shell", "Error: failed to open existing database to migrate data from."));
530 return false;
531 }
532 auto destStore = prepareDatabase(destConfig.get());
533 if (!destStore) {
534 emitError(i18nc("@info:shell", "Error: failed to open new database to migrate data to."));
535 return false;
536 }
537
538 // Run StorageJanitor on the existing database to ensure it's in a consistent state
539 emitInfo(i18nc("@info:status", "Running fsck on the source database"));
540 runStorageJanitor(sourceConfig.get());
541
542 const bool migrationSuccess = migrateTables(sourceStore.get(), destStore.get(), destConfig.get());
543
544 // Stop database servers and close connections. Make sure we always reach here, even if the migration failed
545 cleanupDatabase(sourceStore.get(), sourceConfig.get());
546 cleanupDatabase(destStore.get(), destConfig.get());
547
548 if (!migrationSuccess) {
549 return false;
550 }
551
552 // Remove the migrated database if the migration or post-migration steps fail
553 rollback.add([this, dbPath = destConfig->databasePath()]() {
554 std::error_code ec;
555 std::filesystem::remove_all(dbPath.toStdString(), ec);
556 if (ec) {
557 emitError(i18nc("@info:status %2 is error message",
558 "Error: failed to remove temporary database directory %1: %2",
559 dbPath,
560 QString::fromStdString(ec.message())));
561 }
562 });
563
564 // Move the old database into backup location
565 emitInfo(i18nc("@info:shell", "Backing up original database..."));
566 const auto backupPath = moveDatabaseToBackupLocation(sourceConfig.get());
567 if (!backupPath.has_value()) {
568 return false;
569 }
570
571 if (!backupPath->isEmpty()) {
572 rollback.add([this, backupPath = *backupPath, sourceDbPath]() {
573 emitInfo(i18nc("@info:status", "Restoring database from backup %1 to %2", backupPath, sourceDbPath));
574 if (const auto ec = restoreDatabaseFromBackup(backupPath, sourceDbPath); ec) {
575 emitError(i18nc("@info:status %1 is error message", "Error: failed to restore database from backup: %1", QString::fromStdString(ec.message())));
576 }
577 });
578 }
579
580 // Move the new database to the main location
581 if (!moveDatabaseToMainLocation(destConfig.get(), destServerCfgFile)) {
582 return false;
583 }
584
585 // Migration was success, nothing to roll back.
586 rollback.reset();
587
588 return true;
589}
590
591bool DbMigrator::migrateTables(DataStore *sourceStore, DataStore *destStore, DbConfig *destConfig)
592{
593 // Disable foreign key constraint checks
594 if (!destConfig->disableConstraintChecks(destStore->database())) {
595 return false;
596 }
597
598 AkonadiSchema schema;
599 const int totalTables = schema.tables().size() + schema.relations().size();
600 int doneTables = 0;
601
602 // Copy regular tables
603 for (const auto &table : schema.tables()) {
604 ++doneTables;
605 emitProgress(table.name, doneTables, totalTables);
606 if (!copyTable(sourceStore, destStore, table)) {
607 emitError(i18nc("@info:shell", "Error has occurred while migrating table %1", table.name));
608 return false;
609 }
610 }
611
612 // Copy relational tables
613 for (const auto &relation : schema.relations()) {
614 const RelationTableDescription table{relation};
615 ++doneTables;
616 emitProgress(table.name, doneTables, totalTables);
617 if (!copyTable(sourceStore, destStore, table)) {
618 emitError(i18nc("@info:shell", "Error has occurred while migrating table %1", table.name));
619 return false;
620 }
621 }
622
623 // Re-enable foreign key constraint checks
624 if (!destConfig->enableConstraintChecks(destStore->database())) {
625 return false;
626 }
627
628 return true;
629}
630
631std::optional<QString> DbMigrator::moveDatabaseToBackupLocation(DbConfig *config)
632{
633 const std::filesystem::path dbPath = config->databasePath().toStdString();
634
635 QDir backupDir = StandardDirs::saveDir("data", QStringLiteral("migration_backup"));
636 if (!backupDir.isEmpty()) {
637 const auto result = questionYesNoSkip(i18nc("@label", "Backup directory already exists. Do you want to overwrite the previous backup?"));
638 if (result == UIDelegate::Result::Skip) {
639 return QString{};
640 }
641 if (result == UIDelegate::Result::No) {
642 emitError(i18nc("@info:shell", "Cannot proceed without backup. Migration interrupted."));
643 return {};
644 }
645
646 if (!backupDir.removeRecursively()) {
647 emitError(i18nc("@info:shell", "Failed to remove previous backup directory."));
648 return {};
649 }
650 }
651
652 backupDir.mkpath(QStringLiteral("."));
653
654 std::error_code ec;
655 std::filesystem::path backupPath = backupDir.absolutePath().toStdString();
656 // /path/to/akonadi.sql -> /path/to/backup/akonadi.sql
657 // /path/to/db_data -> /path/to/backup/db_data
658 std::filesystem::rename(dbPath, backupPath / *(--dbPath.end()), ec);
659 if (ec) {
660 emitError(i18nc("@info:shell", "Failed to move database to backup location: %1", QString::fromStdString(ec.message())));
661 return {};
662 }
663
664 return backupDir.absolutePath();
665}
666
667std::optional<QString> DbMigrator::backupAkonadiServerRc()
668{
669 const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
670 const auto bkpFileName = QStringLiteral("%1.bkp").arg(origFileName);
671 std::error_code ec;
672
673 if (QFile::exists(bkpFileName)) {
674 const auto result = questionYesNo(i18nc("@label", "Backup file %1 already exists. Overwrite?", bkpFileName));
675 if (result != UIDelegate::Result::Yes) {
676 emitError(i18nc("@info:status", "Cannot proceed without backup. Migration interrupted."));
677 return {};
678 }
679
680 std::filesystem::remove(bkpFileName.toStdString(), ec);
681 if (ec) {
682 emitError(i18nc("@info:status", "Error: Failed to remove existing backup file %1: %2", bkpFileName, QString::fromStdString(ec.message())));
683 return {};
684 }
685 }
686
687 std::filesystem::copy(origFileName.toStdString(), bkpFileName.toStdString(), ec);
688 if (ec) {
689 emitError(i18nc("@info:status", "Failed to back up Akonadi Server configuration: %1", QString::fromStdString(ec.message())));
690 return {};
691 }
692
693 return bkpFileName;
694}
695
696bool DbMigrator::moveDatabaseToMainLocation(DbConfig *destConfig, const QString &destServerCfgFile)
697{
698 std::error_code ec;
699 // /path/to/db_migration/akonadi.db -> /path/to/akonadi.db
700 // /path/to/db_migration/db_data -> /path/to/db_data
701 const std::filesystem::path dbSrcPath = destConfig->databasePath().toStdString();
702 const auto dbDestPath = dbSrcPath.parent_path().parent_path() / *(--dbSrcPath.end());
703 std::filesystem::rename(dbSrcPath, dbDestPath, ec);
704 if (ec) {
705 emitError(i18nc("@info:status %1 is error message",
706 "Error: failed to move migrated database to the primary location: %1",
707 QString::fromStdString(ec.message())));
708 return false;
709 }
710
711 // Adjust the db path stored in new akonadiserverrc to point to the primary location
712 {
713 QSettings settings(destServerCfgFile, QSettings::IniFormat);
714 destConfig->setDatabasePath(QString::fromStdString(dbDestPath.string()), settings);
715 }
716
717 // Remove the - now empty - db_migration directory
718 // We don't concern ourselves too much with this failing.
719 std::filesystem::remove(dbSrcPath.parent_path(), ec);
720
721 // Turn the new temporary akonadiserverrc int othe main one so that
722 // Akonadi starts with the new DB configuration.
723 std::filesystem::remove(StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
724 if (ec) {
725 emitError(i18nc("@info:status %1 is error message", "Error: failed to remove original akonadiserverrc: %1", QString::fromStdString(ec.message())));
726 return false;
727 }
728 std::filesystem::rename(destServerCfgFile.toStdString(), StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
729 if (ec) {
730 emitError(i18nc("@info:status %1 is error message",
731 "Error: failed to move new akonadiserverrc to the primary location: %1",
732 QString::fromStdString(ec.message())));
733 return false;
734 }
735
736 return true;
737}
738
739bool DbMigrator::copyTable(DataStore *sourceStore, DataStore *destStore, const TableDescription &table)
740{
741 const auto columns = table.columns | Views::transform([](const auto &tbl) {
742 return tbl.name;
743 })
744 | Actions::toQList;
745
746 // Count number of items in the current table
747 const auto totalRows = [](DataStore *store, const QString &table) {
748 CountQueryBuilder countQb(store, table);
749 countQb.exec();
750 return countQb.result();
751 }(sourceStore, table.name);
752
753 // Fetch *everything* from the current able
754 QueryBuilder sourceQb(sourceStore, table.name);
755 sourceQb.addColumns(columns);
756 sourceQb.exec();
757 auto &sourceQuery = sourceQb.query();
758
759 // Clean the destination table (from data pre-inserted by DbInitializer)
760 {
761 QueryBuilder clearQb(destStore, table.name, QueryBuilder::Delete);
762 clearQb.exec();
763 }
764
765 // Begin insertion transaction
766 Transaction transaction(destStore, QStringLiteral("Migrator"));
767 size_t trxSize = 0;
768 size_t processed = 0;
769
770 // Loop over source resluts
771 while (sourceQuery.next()) {
772 // Insert the current row into the new database
773 QueryBuilder destQb(destStore, table.name, QueryBuilder::Insert);
774 destQb.setIdentificationColumn({});
775 for (int col = 0; col < table.columns.size(); ++col) {
776 QVariant value;
777 if (table.columns[col].type == QLatin1StringView("QDateTime")) {
778 value = sourceQuery.value(col).toDateTime();
779 } else if (table.columns[col].type == QLatin1StringView("bool")) {
780 value = sourceQuery.value(col).toBool();
781 } else if (table.columns[col].type == QLatin1StringView("QByteArray")) {
782 value = Utils::variantToByteArray(sourceQuery.value(col));
783 } else if (table.columns[col].type == QLatin1StringView("QString")) {
784 value = Utils::variantToString(sourceQuery.value(col));
785 } else {
786 value = sourceQuery.value(col);
787 }
788 destQb.setColumnValue(table.columns[col].name, value);
789 }
790 if (!destQb.exec()) {
791 qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to insert row into table" << table.name << ":" << destQb.query().lastError().text();
792 return false;
793 }
794
795 // Commit the transaction after every "maxTransactionSize" inserts to make it reasonably fast
796 if (++trxSize > maxTransactionSize) {
797 if (!transaction.commit()) {
798 qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to commit transaction:" << destStore->database().lastError().text();
799 return false;
800 }
801 trxSize = 0;
802 transaction.begin();
803 }
804
805 emitTableProgress(table.name, ++processed, totalRows);
806 }
807
808 // Commit whatever is left in the transaction
809 if (!transaction.commit()) {
810 qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to commit transaction:" << destStore->database().lastError().text();
811 return false;
812 }
813
814 // Synchronize next autoincrement value (if the table has one)
815 if (const auto cnt = std::count_if(table.columns.begin(),
816 table.columns.end(),
817 [](const auto &col) {
818 return col.isAutoIncrement;
819 });
820 cnt == 1) {
821 if (!syncAutoIncrementValue(sourceStore, destStore, table)) {
822 emitError(i18nc("@info:status", "Error: failed to update autoincrement value for table %1", table.name));
823 return false;
824 }
825 }
826
827 emitInfo(i18nc("@info:status", "Optimizing table %1...", table.name));
828 // Optimize the new table
829 if (!analyzeTable(table.name, destStore)) {
830 emitError(i18nc("@info:status", "Error: failed to optimize table %1", table.name));
831 return false;
832 }
833
834 return true;
835}
836
837void DbMigrator::emitInfo(const QString &message)
838{
840 this,
841 [this, message]() {
842 Q_EMIT info(message);
843 },
845}
846
847void DbMigrator::emitError(const QString &message)
848{
850 this,
851 [this, message]() {
852 Q_EMIT error(message);
853 },
855}
856
857void DbMigrator::emitProgress(const QString &table, int done, int total)
858{
860 this,
861 [this, table, done, total]() {
862 Q_EMIT progress(table, done, total);
863 },
865}
866
867void DbMigrator::emitTableProgress(const QString &table, int done, int total)
868{
870 this,
871 [this, table, done, total]() {
872 Q_EMIT tableProgress(table, done, total);
873 },
875}
876
877void DbMigrator::emitCompletion(bool success)
878{
880 this,
881 [this, success]() {
882 Q_EMIT migrationCompleted(success);
883 },
885}
886
887UIDelegate::Result DbMigrator::questionYesNo(const QString &question)
888{
889 UIDelegate::Result answer;
891 this,
892 [this, question, &answer]() {
893 if (m_uiDelegate) {
894 answer = m_uiDelegate->questionYesNo(question);
895 }
896 },
898 return answer;
899}
900
901UIDelegate::Result DbMigrator::questionYesNoSkip(const QString &question)
902{
903 UIDelegate::Result answer;
905 this,
906 [this, question, &answer]() {
907 if (m_uiDelegate) {
908 answer = m_uiDelegate->questionYesNoSkip(question);
909 }
910 },
912 return answer;
913}
914#include "moc_dbmigrator.cpp"
This class handles all the database access.
Definition datastore.h:95
void close()
Closes the database connection.
QSqlDatabase database()
Returns the QSqlDatabase object.
A base class that provides an unique access layer to configuration and initialization of different da...
Definition dbconfig.h:21
virtual void setup()
This method is called to setup initial database settings after a connection is established.
Definition dbconfig.cpp:133
virtual void apply(QSqlDatabase &database)=0
This method applies the configured settings to the QtSql database instance.
virtual QString driverName() const =0
Returns the name of the used driver.
virtual QString databaseName() const =0
Returns the database name.
virtual void stopInternalServer()
This method is called to stop the external server.
Definition dbconfig.cpp:128
virtual bool enableConstraintChecks(const QSqlDatabase &db)=0
Re-enables foreign key constraint checks.
virtual bool disableConstraintChecks(const QSqlDatabase &db)=0
Disables foreign key constraint checks.
virtual void setDatabasePath(const QString &path, QSettings &settings)=0
Set the path to the database file or directory.
virtual QString databasePath() const =0
Returns path to the database file or directory.
virtual bool startInternalServer()
This method is called to start an external server.
Definition dbconfig.cpp:122
virtual bool useInternalServer() const =0
Returns whether an internal server needs to be used.
static DbIntrospector::Ptr createInstance(const QSqlDatabase &database)
Returns an introspector instance for a given database.
A helper class that describes a table for the DbInitializer.
Definition schematypes.h:77
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Definition dbtype.cpp:11
Helper integration between Akonadi and Qt.
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QString path(const QString &relativePath)
KIOCORE_EXPORT void add(const QString &fileClass, const QString &directory)
KGuiItem reset()
QDBusConnectionInterface * interface() const const
QDBusConnection sessionBus()
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
QDBusReply< QDBusConnectionInterface::RegisterServiceReply > registerService(const QString &serviceName, ServiceQueueOptions qoption, ServiceReplacementOptions roption)
QDBusReply< void > startService(const QString &name)
QDBusReply< bool > unregisterService(const QString &serviceName)
QString absolutePath() const const
bool isEmpty(Filters filters) const const
bool mkpath(const QString &dirPath) const const
bool removeRecursively()
Duration durationElapsed() const const
bool exists() const const
bool remove()
bool exists(const QString &path)
T value(qsizetype i) const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QSqlDatabase addDatabase(QSqlDriver *driver, const QString &connectionName)
QSqlError lastError() const const
QString text() const const
QString fromStdString(const std::string &str)
bool isEmpty() const const
QString toLower() const const
std::string toStdString() const const
QueuedConnection
QFuture< T > run(Function function,...)
QThread * create(Function &&f, Args &&... args)
void msleep(unsigned long msecs)
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:53:09 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.