File indexing completed on 2025-01-05 04:46:57

0001 /***************************************************************************
0002  *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
0003  *   SPDX-FileCopyrightText: 2012 Volker Krause <vkrause@kde.org>          *
0004  *                                                                         *
0005  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
0006  ***************************************************************************/
0007 
0008 #include "dbinitializer.h"
0009 #include "akonadiserver_debug.h"
0010 #include "dbexception.h"
0011 #include "dbinitializer_p.h"
0012 #include "dbtype.h"
0013 #include "entities.h"
0014 #include "schema.h"
0015 #include "storage/datastore.h"
0016 
0017 #include <QDateTime>
0018 #include <QSqlQuery>
0019 #include <QStringList>
0020 
0021 #include <algorithm>
0022 
0023 #include "private/tristate_p.h"
0024 
0025 using namespace Akonadi::Server;
0026 
0027 DbInitializer::Ptr DbInitializer::createInstance(const QSqlDatabase &database, Schema *schema)
0028 {
0029     DbInitializer::Ptr i;
0030     switch (DbType::type(database)) {
0031     case DbType::MySQL:
0032         i.reset(new DbInitializerMySql(database));
0033         break;
0034     case DbType::Sqlite:
0035         i.reset(new DbInitializerSqlite(database));
0036         break;
0037     case DbType::PostgreSQL:
0038         i.reset(new DbInitializerPostgreSql(database));
0039         break;
0040     case DbType::Unknown:
0041         qCCritical(AKONADISERVER_LOG) << database.driverName() << "backend not supported";
0042         break;
0043     }
0044     i->mSchema = schema;
0045     return i;
0046 }
0047 
0048 DbInitializer::DbInitializer(const QSqlDatabase &database)
0049     : mDatabase(database)
0050     , mSchema(nullptr)
0051     , mTestInterface(nullptr)
0052 {
0053     m_introspector = DbIntrospector::createInstance(mDatabase);
0054 }
0055 
0056 DbInitializer::~DbInitializer()
0057 {
0058 }
0059 
0060 bool DbInitializer::run()
0061 {
0062     try {
0063         qCInfo(AKONADISERVER_LOG) << "Running DB initializer";
0064 
0065         const auto tables = mSchema->tables();
0066         for (const TableDescription &table : tables) {
0067             if (!checkTable(table)) {
0068                 return false;
0069             }
0070         }
0071 
0072         const auto relations = mSchema->relations();
0073         for (const RelationDescription &relation : relations) {
0074             if (!checkRelation(relation)) {
0075                 return false;
0076             }
0077         }
0078 
0079 #ifndef DBINITIALIZER_UNITTEST
0080         // Now finally check and set the generation identifier if necessary
0081         auto store = DataStore::dataStoreForDatabase(mDatabase);
0082         SchemaVersion version = SchemaVersion::retrieveAll(store).at(0);
0083         if (version.generation() == 0) {
0084             version.setGeneration(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
0085             version.update(store);
0086 
0087             qCDebug(AKONADISERVER_LOG) << "Generation:" << version.generation();
0088         }
0089 #endif
0090 
0091         qCInfo(AKONADISERVER_LOG) << "DB initializer done";
0092         return true;
0093     } catch (const DbException &e) {
0094         mErrorMsg = QString::fromUtf8(e.what());
0095     }
0096     return false;
0097 }
0098 
0099 bool DbInitializer::checkTable(const TableDescription &tableDescription)
0100 {
0101     qCDebug(AKONADISERVER_LOG) << "checking table " << tableDescription.name;
0102 
0103     if (!m_introspector->hasTable(tableDescription.name)) {
0104         // Get the CREATE TABLE statement for the specific SQL dialect
0105         const QString createTableStatement = buildCreateTableStatement(tableDescription);
0106         qCDebug(AKONADISERVER_LOG) << createTableStatement;
0107         execQuery(createTableStatement);
0108     } else {
0109         // Check for every column whether it exists, and add the missing ones
0110         for (const ColumnDescription &columnDescription : tableDescription.columns) {
0111             if (!m_introspector->hasColumn(tableDescription.name, columnDescription.name)) {
0112                 // Don't add the column on update, DbUpdater will add it
0113                 if (columnDescription.noUpdate) {
0114                     continue;
0115                 }
0116                 // Get the ADD COLUMN statement for the specific SQL dialect
0117                 const QString statement = buildAddColumnStatement(tableDescription, columnDescription);
0118                 qCDebug(AKONADISERVER_LOG) << statement;
0119                 execQuery(statement);
0120             }
0121         }
0122 
0123         // NOTE: we do intentionally not delete any columns here, we defer that to the updater,
0124         // very likely previous columns contain data that needs to be moved to a new column first.
0125     }
0126 
0127     // Add initial data if table is empty
0128     if (tableDescription.data.isEmpty()) {
0129         return true;
0130     }
0131     if (m_introspector->isTableEmpty(tableDescription.name)) {
0132         for (const DataDescription &dataDescription : tableDescription.data) {
0133             // Get the INSERT VALUES statement for the specific SQL dialect
0134             const QString statement = buildInsertValuesStatement(tableDescription, dataDescription);
0135             qCDebug(AKONADISERVER_LOG) << statement;
0136             execQuery(statement);
0137         }
0138     }
0139 
0140     return true;
0141 }
0142 
0143 void DbInitializer::checkForeignKeys(const TableDescription &tableDescription)
0144 {
0145     try {
0146         const QList<DbIntrospector::ForeignKey> existingForeignKeys = m_introspector->foreignKeyConstraints(tableDescription.name);
0147         for (const ColumnDescription &column : tableDescription.columns) {
0148             DbIntrospector::ForeignKey existingForeignKey;
0149             for (const DbIntrospector::ForeignKey &fk : existingForeignKeys) {
0150                 if (QString::compare(fk.column, column.name, Qt::CaseInsensitive) == 0) {
0151                     existingForeignKey = fk;
0152                     break;
0153                 }
0154             }
0155 
0156             if (!column.refTable.isEmpty() && !column.refColumn.isEmpty()) {
0157                 if (!existingForeignKey.column.isEmpty()) {
0158                     // there's a constraint on this column, check if it's the correct one
0159                     if (QString::compare(existingForeignKey.refTable, column.refTable + QLatin1StringView("table"), Qt::CaseInsensitive) == 0
0160                         && QString::compare(existingForeignKey.refColumn, column.refColumn, Qt::CaseInsensitive) == 0
0161                         && QString::compare(existingForeignKey.onUpdate, referentialActionToString(column.onUpdate), Qt::CaseInsensitive) == 0
0162                         && QString::compare(existingForeignKey.onDelete, referentialActionToString(column.onDelete), Qt::CaseInsensitive) == 0) {
0163                         continue; // all good
0164                     }
0165 
0166                     const auto statements = buildRemoveForeignKeyConstraintStatements(existingForeignKey, tableDescription);
0167                     if (!statements.isEmpty()) {
0168                         qCDebug(AKONADISERVER_LOG) << "Found existing foreign constraint that doesn't match the schema:" << existingForeignKey.name
0169                                                    << existingForeignKey.column << existingForeignKey.refTable << existingForeignKey.refColumn;
0170                         m_removedForeignKeys << statements;
0171                     }
0172                 }
0173 
0174                 const auto statements = buildAddForeignKeyConstraintStatements(tableDescription, column);
0175                 if (statements.isEmpty()) { // not supported
0176                     return;
0177                 }
0178 
0179                 m_pendingForeignKeys << statements;
0180 
0181             } else if (!existingForeignKey.column.isEmpty()) {
0182                 // constraint exists but we don't want one here
0183                 const auto statements = buildRemoveForeignKeyConstraintStatements(existingForeignKey, tableDescription);
0184                 if (!statements.isEmpty()) {
0185                     qCDebug(AKONADISERVER_LOG) << "Found unexpected foreign key constraint:" << existingForeignKey.name << existingForeignKey.column
0186                                                << existingForeignKey.refTable << existingForeignKey.refColumn;
0187                     m_removedForeignKeys << statements;
0188                 }
0189             }
0190         }
0191     } catch (const DbException &e) {
0192         qCDebug(AKONADISERVER_LOG) << "Fixing foreign key constraints failed:" << e.what();
0193     }
0194 }
0195 
0196 void DbInitializer::checkIndexes(const TableDescription &tableDescription)
0197 {
0198     // Add indices
0199     for (const IndexDescription &indexDescription : tableDescription.indexes) {
0200         // sqlite3 needs unique index identifiers per db
0201         const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name);
0202         if (!m_introspector->hasIndex(tableDescription.name, indexName)) {
0203             // Get the CREATE INDEX statement for the specific SQL dialect
0204             m_pendingIndexes << buildCreateIndexStatement(tableDescription, indexDescription);
0205         }
0206     }
0207 }
0208 
0209 bool DbInitializer::checkRelation(const RelationDescription &relationDescription)
0210 {
0211     return checkTable(RelationTableDescription(relationDescription));
0212 }
0213 
0214 QString DbInitializer::errorMsg() const
0215 {
0216     return mErrorMsg;
0217 }
0218 
0219 bool DbInitializer::updateIndexesAndConstraints()
0220 {
0221     const auto tables = mSchema->tables();
0222     for (const TableDescription &table : tables) {
0223         // Make sure the foreign key constraints are all there
0224         checkForeignKeys(table);
0225         checkIndexes(table);
0226     }
0227     const auto relations = mSchema->relations();
0228     for (const RelationDescription &relation : relations) {
0229         RelationTableDescription relTable(relation);
0230         checkForeignKeys(relTable);
0231         checkIndexes(relTable);
0232     }
0233 
0234     try {
0235         if (!m_pendingIndexes.isEmpty()) {
0236             qCDebug(AKONADISERVER_LOG) << "Updating indexes";
0237             execPendingQueries(m_pendingIndexes);
0238             m_pendingIndexes.clear();
0239         }
0240 
0241         if (!m_removedForeignKeys.isEmpty()) {
0242             qCDebug(AKONADISERVER_LOG) << "Removing invalid foreign key constraints";
0243             execPendingQueries(m_removedForeignKeys);
0244             m_removedForeignKeys.clear();
0245         }
0246 
0247         if (!m_pendingForeignKeys.isEmpty()) {
0248             qCDebug(AKONADISERVER_LOG) << "Adding new foreign key constraints";
0249             execPendingQueries(m_pendingForeignKeys);
0250             m_pendingForeignKeys.clear();
0251         }
0252     } catch (const DbException &e) {
0253         qCCritical(AKONADISERVER_LOG) << "Updating index failed: " << e.what();
0254         return false;
0255     }
0256 
0257     qCDebug(AKONADISERVER_LOG) << "Indexes successfully created";
0258     return true;
0259 }
0260 
0261 void DbInitializer::execPendingQueries(const QStringList &queries)
0262 {
0263     for (const QString &statement : queries) {
0264         qCDebug(AKONADISERVER_LOG) << statement;
0265         execQuery(statement);
0266     }
0267 }
0268 
0269 QString DbInitializer::sqlType(const ColumnDescription &col, int size) const
0270 {
0271     Q_UNUSED(size)
0272     if (col.type == QLatin1StringView("int")) {
0273         return QStringLiteral("INTEGER");
0274     }
0275     if (col.type == QLatin1StringView("qint64")) {
0276         return QStringLiteral("BIGINT");
0277     }
0278     if (col.type == QLatin1StringView("QString")) {
0279         return QStringLiteral("TEXT");
0280     }
0281     if (col.type == QLatin1StringView("QByteArray")) {
0282         return QStringLiteral("LONGBLOB");
0283     }
0284     if (col.type == QLatin1StringView("QDateTime")) {
0285         return QStringLiteral("TIMESTAMP");
0286     }
0287     if (col.type == QLatin1StringView("bool")) {
0288         return QStringLiteral("BOOL");
0289     }
0290     if (col.isEnum) {
0291         return QStringLiteral("TINYINT");
0292     }
0293 
0294     qCCritical(AKONADISERVER_LOG) << "Invalid type" << col.type;
0295     Q_ASSERT(false);
0296     return QString();
0297 }
0298 
0299 QString DbInitializer::sqlValue(const ColumnDescription &col, const QString &value) const
0300 {
0301     if (col.type == QLatin1StringView("QDateTime") && value == QLatin1StringView("QDateTime::currentDateTimeUtc()")) {
0302         return QStringLiteral("CURRENT_TIMESTAMP");
0303     } else if (col.isEnum) {
0304         return QString::number(col.enumValueMap[value]);
0305     }
0306     return value;
0307 }
0308 
0309 QString DbInitializer::buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const
0310 {
0311     return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2").arg(tableDescription.name, buildColumnStatement(columnDescription, tableDescription));
0312 }
0313 
0314 QString DbInitializer::buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const
0315 {
0316     const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name);
0317     QStringList columns;
0318     if (indexDescription.sort.isEmpty()) {
0319         columns = indexDescription.columns;
0320     } else {
0321         columns.reserve(indexDescription.columns.count());
0322         std::transform(indexDescription.columns.cbegin(),
0323                        indexDescription.columns.cend(),
0324                        std::back_insert_iterator<QStringList>(columns),
0325                        [&indexDescription](const QString &column) {
0326                            return QStringLiteral("%1 %2").arg(column, indexDescription.sort);
0327                        });
0328     }
0329 
0330     return QStringLiteral("CREATE %1 INDEX %2 ON %3 (%4)")
0331         .arg(indexDescription.isUnique ? QStringLiteral("UNIQUE") : QString(), indexName, tableDescription.name, columns.join(QLatin1Char(',')));
0332 }
0333 
0334 QStringList DbInitializer::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const
0335 {
0336     Q_UNUSED(table)
0337     Q_UNUSED(column)
0338     return {};
0339 }
0340 
0341 QStringList DbInitializer::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const
0342 {
0343     Q_UNUSED(fk)
0344     Q_UNUSED(table)
0345     return {};
0346 }
0347 
0348 QString DbInitializer::buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete)
0349 {
0350     return QLatin1StringView("ON UPDATE ") + referentialActionToString(onUpdate) + QLatin1StringView(" ON DELETE ") + referentialActionToString(onDelete);
0351 }
0352 
0353 QString DbInitializer::referentialActionToString(ColumnDescription::ReferentialAction action)
0354 {
0355     switch (action) {
0356     case ColumnDescription::Cascade:
0357         return QStringLiteral("CASCADE");
0358     case ColumnDescription::Restrict:
0359         return QStringLiteral("RESTRICT");
0360     case ColumnDescription::SetNull:
0361         return QStringLiteral("SET NULL");
0362     }
0363 
0364     Q_ASSERT(!"invalid referential action enum!");
0365     return QString();
0366 }
0367 
0368 QString DbInitializer::buildPrimaryKeyStatement(const TableDescription &table)
0369 {
0370     QStringList cols;
0371     for (const ColumnDescription &column : std::as_const(table.columns)) {
0372         if (column.isPrimaryKey) {
0373             cols.push_back(column.name);
0374         }
0375     }
0376     return QLatin1StringView("PRIMARY KEY (") + cols.join(QLatin1StringView(", ")) + QLatin1Char(')');
0377 }
0378 
0379 void DbInitializer::execQuery(const QString &queryString)
0380 {
0381     // if ( Q_UNLIKELY( mTestInterface ) ) { Qt 4.7 has no Q_UNLIKELY yet
0382     if (mTestInterface) {
0383         mTestInterface->execStatement(queryString);
0384         return;
0385     }
0386 
0387     QSqlQuery query(mDatabase);
0388     if (!query.exec(queryString)) {
0389         throw DbException(query);
0390     }
0391 }
0392 
0393 void DbInitializer::setTestInterface(TestInterface *interface)
0394 {
0395     mTestInterface = interface;
0396 }
0397 
0398 void DbInitializer::setIntrospector(const DbIntrospector::Ptr &introspector)
0399 {
0400     m_introspector = introspector;
0401 }