9#include "ControlManager.h"
10#include "akonadidbmigrator_debug.h"
11#include "akonadifull-version.h"
12#include "akonadischema.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"
30#include <QCommandLineOption>
31#include <QCommandLineParser>
32#include <QCoreApplication>
33#include <QDBusConnection>
35#include <QDBusInterface>
37#include <QElapsedTimer>
42#include <QSqlDatabase>
48#include <KLocalizedString>
51#include <qdbusconnection.h>
54using namespace Akonadi::Server;
55using namespace AkRanges;
56using namespace std::chrono_literals;
58Q_DECLARE_METATYPE(UIDelegate::Result);
63constexpr size_t maxTransactionSize = 1000;
65class MigratorDataStoreFactory :
public DataStoreFactory
68 explicit MigratorDataStoreFactory(
DbConfig *config)
75 class MigratorDataStore :
public DataStore
78 explicit MigratorDataStore(
DbConfig *config)
83 return new MigratorDataStore(m_dbConfig);
92 Q_DISABLE_COPY_MOVE(Rollback)
108 for (
auto it = mRollbacks.rbegin(); it != mRollbacks.rend(); ++it) {
115 void add(T &&rollback)
117 mRollbacks.push_back(std::forward<T>(rollback));
121 std::vector<std::function<void()>> mRollbacks;
124void stopDatabaseServer(
DbConfig *config)
129bool createDatabase(
DbConfig *config)
135 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Invalid database object during initial database connection";
139 const auto closeDb = qScopeGuard([&db]() {
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...";
158 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Failed to connect to database!";
159 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Database error:" << db.lastError().text();
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();
174std::unique_ptr<DataStore> prepareDatabase(
DbConfig *config)
178 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Failed to start the database server";
182 if (!createDatabase(config)) {
189 auto factory = std::make_unique<MigratorDataStoreFactory>(config);
190 std::unique_ptr<DataStore> store{factory->createStore()};
191 if (!store->database().isOpen()) {
194 if (!store->init()) {
205 stopDatabaseServer(config);
210 const auto idCol = std::find_if(table.columns.begin(), table.columns.end(), [](
const auto &col) {
211 return col.isPrimaryKey;
213 if (idCol == table.columns.end()) {
217 const auto getAutoIncrementValue = [](
DataStore *store,
const QString &table,
const QString &idCol) -> std::optional<qint64> {
222 if (!
query.exec(introspector->getAutoIncrementValueQuery(table, idCol))) {
223 qCCritical(AKONADIDBMIGRATOR_LOG) <<
query.lastError().text();
235 const auto setAutoIncrementValue = [](
DataStore *store,
const QString &table,
const QString &idCol, qint64 seq) ->
bool {
240 if (!
query.exec(introspector->updateAutoIncrementValueQuery(table, idCol, seq))) {
241 qCritical(AKONADIDBMIGRATOR_LOG) <<
query.lastError().text();
248 const auto seq = getAutoIncrementValue(sourceStore, table.name, idCol->name);
253 return setAutoIncrementValue(destStore, table.name, idCol->name, *seq);
262 case DbType::PostgreSQL:
268 case DbType::Unknown:
269 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Unknown database type";
274 if (!
query.exec(queryStr)) {
275 qCCritical(AKONADIDBMIGRATOR_LOG) <<
query.lastError().text();
284 const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
285 const auto newFileName = QStringLiteral(
"%1.new").arg(origFileName);
288 settings.setValue(QStringLiteral(
"General/Driver"), targetEngine);
295 const auto enginelc = engine.
toLower();
297 return QStringLiteral(
"QSQLITE");
300 return QStringLiteral(
"QMYSQL");
303 return QStringLiteral(
"QPSQL");
306 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Invalid engine:" << engine;
310std::unique_ptr<DbConfig> dbConfigFromServerRc(
const QString &configFile,
bool overrideDbPath =
false)
313 const auto driver = settings.value(QStringLiteral(
"General/Driver")).toString();
314 std::unique_ptr<DbConfig> config;
317 config = std::make_unique<DbConfigSqlite>(configFile);
319 config = std::make_unique<DbConfigMysql>(configFile);
320 dbPathSuffix = QStringLiteral(
"/db_data");
322 config = std::make_unique<DbConfigPostgresql>(configFile);
323 dbPathSuffix = QStringLiteral(
"/db_data");
325 qCCritical(AKONADIDBMIGRATOR_LOG) <<
"Invalid driver: " << driver;
329 const auto dbPath = overrideDbPath ? StandardDirs::saveDir(
"data", QStringLiteral(
"db_migration%1").arg(dbPathSuffix)) :
QString{};
330 config->init(settings,
true, dbPath);
335bool isValidDbPath(
const QString &path)
342 return fi.exists() && (fi.isFile() || fi.isDir());
345std::error_code restoreDatabaseFromBackup(
const QString &backupPath,
const QString &originalPath)
350 std::filesystem::remove_all(originalPath.
toStdString(), ec);
359bool akonadiIsRunning()
362 return sessionIface->
isServiceRegistered(DBus::serviceName(DBus::ControlLock)) || sessionIface->isServiceRegistered(DBus::serviceName(DBus::Server));
367 static constexpr auto shutdownTimeout = 5s;
369 org::freedesktop::Akonadi::ControlManager manager(DBus::serviceName(DBus::Control), QStringLiteral(
"/ControlManager"),
QDBusConnection::sessionBus());
370 if (!manager.isValid()) {
378 while (akonadiIsRunning() && timer.
durationElapsed() <= shutdownTimeout) {
382 return timer.
durationElapsed() <= shutdownTimeout && !akonadiIsRunning();
391bool acquireAkonadiLock()
394 auto reply = connIface->
registerService(DBus::serviceName(DBus::ControlLock));
399 reply = connIface->registerService(DBus::serviceName(DBus::UpgradeIndicator));
407bool releaseAkonadiLock()
411 connIface->unregisterService(DBus::serviceName(DBus::UpgradeIndicator));
417DbMigrator::DbMigrator(
const QString &targetEngine, UIDelegate *delegate,
QObject *parent)
419 , m_targetEngine(targetEngine)
420 , m_uiDelegate(delegate)
422 qRegisterMetaType<UIDelegate::Result>();
425DbMigrator::~DbMigrator()
432void DbMigrator::startMigration()
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);
446 if (!acquireAkonadiLock()) {
447 emitError(
i18nc(
"@info:status",
"Error: couldn't acquire DBus lock for Akonadi."));
448 emitCompletion(
false);
452 const bool result = runMigrationThread();
454 releaseAkonadiLock();
455 if (restartAkonadi) {
456 emitInfo(
i18nc(
"@info:status",
"Starting Akonadi service..."));
460 emitCompletion(result);
465bool DbMigrator::runStorageJanitor(
DbConfig *dbConfig)
468 connect(&janitor, &StorageJanitor::done,
this, [
this]() {
469 emitInfo(
i18nc(
"@info:status",
"Database fsck completed"));
477bool DbMigrator::runMigrationThread()
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));
488 const auto sourceServerCfgFile = backupAkonadiServerRc();
489 if (!sourceServerCfgFile) {
492 rollback.add([file = *sourceServerCfgFile]() {
497 const QString destServerCfgFile = createTmpAkonadiServerRc(driver);
498 rollback.add([destServerCfgFile]() {
503 auto sourceConfig = dbConfigFromServerRc(*sourceServerCfgFile);
505 emitError(
i18nc(
"@info:shell",
"Error: failed to configure source database server."));
509 if (sourceConfig->driverName() == driver) {
510 emitError(
i18nc(
"@info:shell",
"Source and destination database engines are the same."));
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."));
522 auto destConfig = dbConfigFromServerRc(destServerCfgFile,
true);
524 emitError(
i18nc(
"@info:shell",
"Error: failed to configure the new database server."));
528 auto sourceStore = prepareDatabase(sourceConfig.get());
530 emitError(
i18nc(
"@info:shell",
"Error: failed to open existing database to migrate data from."));
533 auto destStore = prepareDatabase(destConfig.get());
535 emitError(
i18nc(
"@info:shell",
"Error: failed to open new database to migrate data to."));
540 emitInfo(
i18nc(
"@info:status",
"Running fsck on the source database"));
541 runStorageJanitor(sourceConfig.get());
543 const bool migrationSuccess = migrateTables(sourceStore.get(), destStore.get(), destConfig.get());
546 cleanupDatabase(sourceStore.get(), sourceConfig.get());
547 cleanupDatabase(destStore.get(), destConfig.get());
549 if (!migrationSuccess) {
554 rollback.add([
this, dbPath = destConfig->databasePath()]() {
556 std::filesystem::remove_all(dbPath.toStdString(), ec);
558 emitError(i18nc(
"@info:status %2 is error message",
559 "Error: failed to remove temporary database directory %1: %2",
561 QString::fromStdString(ec.message())));
566 emitInfo(
i18nc(
"@info:shell",
"Backing up original database..."));
567 const auto backupPath = moveDatabaseToBackupLocation(sourceConfig.get());
568 if (!backupPath.has_value()) {
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())));
582 if (!moveDatabaseToMainLocation(destConfig.get(), destServerCfgFile)) {
599 AkonadiSchema schema;
600 const int totalTables = schema.tables().size() + schema.relations().size();
604 for (
const auto &table : schema.tables()) {
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));
614 for (
const auto &relation : schema.relations()) {
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));
632std::optional<QString> DbMigrator::moveDatabaseToBackupLocation(
DbConfig *config)
636 QDir backupDir = StandardDirs::saveDir(
"data", QStringLiteral(
"migration_backup"));
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) {
642 if (result == UIDelegate::Result::No) {
643 emitError(
i18nc(
"@info:shell",
"Cannot proceed without backup. Migration interrupted."));
648 emitError(
i18nc(
"@info:shell",
"Failed to remove previous backup directory."));
653 backupDir.
mkpath(QStringLiteral(
"."));
659 std::filesystem::rename(dbPath, backupPath / *(--dbPath.end()), ec);
668std::optional<QString> DbMigrator::backupAkonadiServerRc()
670 const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
671 const auto bkpFileName = QStringLiteral(
"%1.bkp").arg(origFileName);
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."));
681 std::filesystem::remove(bkpFileName.toStdString(), ec);
683 emitError(
i18nc(
"@info:status",
"Error: Failed to remove existing backup file %1: %2", bkpFileName,
QString::fromStdString(ec.message())));
688 std::filesystem::copy(origFileName.toStdString(), bkpFileName.toStdString(), ec);
697bool DbMigrator::moveDatabaseToMainLocation(
DbConfig *destConfig,
const QString &destServerCfgFile)
703 const auto dbDestPath = dbSrcPath.parent_path().parent_path() / *(--dbSrcPath.end());
704 std::filesystem::rename(dbSrcPath, dbDestPath, ec);
706 emitError(
i18nc(
"@info:status %1 is error message",
707 "Error: failed to move migrated database to the primary location: %1",
720 std::filesystem::remove(dbSrcPath.parent_path(), ec);
724 std::filesystem::remove(StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
726 emitError(
i18nc(
"@info:status %1 is error message",
"Error: failed to remove original akonadiserverrc: %1",
QString::fromStdString(ec.message())));
729 std::filesystem::rename(destServerCfgFile.
toStdString(), StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
731 emitError(
i18nc(
"@info:status %1 is error message",
732 "Error: failed to move new akonadiserverrc to the primary location: %1",
742 const auto columns = table.columns | Views::transform([](
const auto &tbl) {
751 return countQb.result();
752 }(sourceStore, table.name);
756 sourceQb.addColumns(columns);
758 auto &sourceQuery = sourceQb.query();
762 QueryBuilder clearQb(destStore, table.name, QueryBuilder::Delete);
767 Transaction transaction(destStore, QStringLiteral(
"Migrator"));
769 size_t processed = 0;
772 while (sourceQuery.next()) {
774 QueryBuilder destQb(destStore, table.name, QueryBuilder::Insert);
775 destQb.setIdentificationColumn({});
776 for (
int col = 0; col < table.columns.size(); ++col) {
779 value = sourceQuery.
value(col).toDateTime();
781 value = sourceQuery.
value(col).toBool();
783 value = Utils::variantToByteArray(sourceQuery.value(col));
785 value = Utils::variantToString(sourceQuery.value(col));
787 value = sourceQuery.
value(col);
789 destQb.setColumnValue(table.columns[col].name, value);
791 if (!destQb.exec()) {
792 qCWarning(AKONADIDBMIGRATOR_LOG) <<
"Failed to insert row into table" << table.name <<
":" << destQb.query().lastError().text();
797 if (++trxSize > maxTransactionSize) {
798 if (!transaction.commit()) {
799 qCWarning(AKONADIDBMIGRATOR_LOG) <<
"Failed to commit transaction:" << destStore->
database().
lastError().
text();
806 emitTableProgress(table.name, ++processed, totalRows);
810 if (!transaction.commit()) {
811 qCWarning(AKONADIDBMIGRATOR_LOG) <<
"Failed to commit transaction:" << destStore->
database().
lastError().
text();
816 if (
const auto cnt = std::count_if(table.columns.begin(),
818 [](
const auto &col) {
819 return col.isAutoIncrement;
822 if (!syncAutoIncrementValue(sourceStore, destStore, table)) {
823 emitError(
i18nc(
"@info:status",
"Error: failed to update autoincrement value for table %1", table.name));
828 emitInfo(
i18nc(
"@info:status",
"Optimizing table %1...", table.name));
830 if (!analyzeTable(table.name, destStore)) {
831 emitError(
i18nc(
"@info:status",
"Error: failed to optimize table %1", table.name));
838void DbMigrator::emitInfo(
const QString &message)
848void DbMigrator::emitError(
const QString &message)
858void DbMigrator::emitProgress(
const QString &table,
int done,
int total)
862 [
this, table, done, total]() {
863 Q_EMIT progress(table, done, total);
868void DbMigrator::emitTableProgress(
const QString &table,
int done,
int total)
872 [
this, table, done, total]() {
873 Q_EMIT tableProgress(table, done, total);
878void DbMigrator::emitCompletion(
bool success)
883 Q_EMIT migrationCompleted(success);
888UIDelegate::Result DbMigrator::questionYesNo(
const QString &question)
890 UIDelegate::Result answer;
893 [
this, question, &answer]() {
895 answer = m_uiDelegate->questionYesNo(question);
902UIDelegate::Result DbMigrator::questionYesNoSkip(
const QString &question)
904 UIDelegate::Result answer;
907 [
this, question, &answer]() {
909 answer = m_uiDelegate->questionYesNoSkip(question);
915#include "moc_dbmigrator.cpp"
Helper class for creating queries to count elements in a database.
This class handles all the database access.
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...
virtual void setup()
This method is called to setup initial database settings after a connection is established.
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.
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.
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.
Helper class for DataStore transaction handling.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
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)
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
Duration durationElapsed() const const
bool exists() const const
bool exists() const const
T value(qsizetype i) const const
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
QFuture< T > run(Function function,...)
QThread * create(Function &&f, Args &&... args)
void msleep(unsigned long msecs)