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

KDE's Doxygen guidelines are available online.