Akonadi

dbupdater.cpp
1/*
2 SPDX-FileCopyrightText: 2007-2012 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "dbupdater.h"
8#include "akonadischema.h"
9#include "akonadiserver_debug.h"
10#include "datastore.h"
11#include "dbconfig.h"
12#include "dbinitializer_p.h"
13#include "dbintrospector.h"
14#include "dbtype.h"
15#include "entities.h"
16#include "querybuilder.h"
17#include "selectquerybuilder.h"
18
19#include "private/dbus_p.h"
20
21#include <QCoreApplication>
22#include <QDBusConnection>
23#include <QDBusError>
24#include <QMetaMethod>
25#include <QSqlError>
26#include <QSqlQuery>
27#include <QThread>
28
29#include <QDomDocument>
30#include <QElapsedTimer>
31#include <QFile>
32#include <QSqlResult>
33
34using namespace Akonadi;
35using namespace Akonadi::Server;
36
37DbUpdater::DbUpdater(const QSqlDatabase &database, const QString &filename)
38 : m_database(database)
39 , m_filename(filename)
40{
41}
42
44{
45 // TODO error handling
46 auto store = DataStore::dataStoreForDatabase(m_database);
47 auto currentVersion = SchemaVersion::retrieveAll(store).at(0);
48
49 UpdateSet::Map updates;
50
51 if (!parseUpdateSets(currentVersion.version(), updates)) {
52 return false;
53 }
54
55 if (updates.isEmpty()) {
56 return true;
57 }
58
59 // indicate clients this might take a while
60 // we can ignore unregistration in error cases, that'll kill the server anyway
61 if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::UpgradeIndicator))) {
62 qCCritical(AKONADISERVER_LOG) << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message();
63 }
64
65 // QMap is sorted, so we should be replaying the changes in correct order
66 for (QMap<int, UpdateSet>::ConstIterator it = updates.constBegin(); it != updates.constEnd(); ++it) {
67 Q_ASSERT(it.key() > currentVersion.version());
68 qCDebug(AKONADISERVER_LOG) << "DbUpdater: update to version:" << it.key() << " mandatory:" << it.value().abortOnFailure;
69
70 bool success = false;
71 bool hasTransaction = false;
72 if (it.value().complex) { // complex update
73 const QString methodName = QStringLiteral("complexUpdate_%1()").arg(it.value().version);
74 const int index = metaObject()->indexOfMethod(methodName.toLatin1().constData());
75 if (index == -1) {
76 success = false;
77 qCCritical(AKONADISERVER_LOG) << "Update to version" << it.value().version << "marked as complex, but no implementation is available";
78 } else {
79 const QMetaMethod method = metaObject()->method(index);
80 method.invoke(this, Q_RETURN_ARG(bool, success));
81 if (!success) {
82 qCCritical(AKONADISERVER_LOG) << "Update failed";
83 }
84 }
85 } else { // regular update
86 success = m_database.transaction();
87 if (success) {
88 hasTransaction = true;
89 const QStringList statements = it.value().statements;
90 for (const QString &statement : statements) {
91 QSqlQuery query(m_database);
92 success = query.exec(statement);
93 if (!success) {
94 qCCritical(AKONADISERVER_LOG) << "DBUpdater: query error:" << query.lastError().text() << m_database.lastError().text();
95 qCCritical(AKONADISERVER_LOG) << "Query was: " << statement;
96 qCCritical(AKONADISERVER_LOG) << "Target version was: " << it.key();
97 qCCritical(AKONADISERVER_LOG) << "Mandatory: " << it.value().abortOnFailure;
98 }
99 }
100 }
101 }
102
103 if (success) {
104 currentVersion.setVersion(it.key());
105 success = currentVersion.update();
106 }
107
108 if (!success || (hasTransaction && !m_database.commit())) {
109 qCCritical(AKONADISERVER_LOG) << "Failed to commit transaction for database update";
110 if (hasTransaction) {
111 m_database.rollback();
112 }
113 if (it.value().abortOnFailure) {
114 return false;
115 }
116 }
117 }
118
119 QDBusConnection::sessionBus().unregisterService(DBus::serviceName(DBus::UpgradeIndicator));
120 return true;
121}
122
123bool DbUpdater::parseUpdateSets(int currentVersion, UpdateSet::Map &updates) const
124{
125 QFile file(m_filename);
126 if (!file.open(QIODevice::ReadOnly)) {
127 qCCritical(AKONADISERVER_LOG) << "Unable to open update description file" << m_filename;
128 return false;
129 }
130
131 QDomDocument document;
132
133 const auto result = document.setContent(&file);
134 if (!result) {
135 qCCritical(AKONADISERVER_LOG) << "Unable to parse update description file" << m_filename << ":" << result.errorMessage << "at line" << result.errorLine
136 << "column" << result.errorColumn;
137 return false;
138 }
139
140 const QDomElement documentElement = document.documentElement();
141 if (documentElement.tagName() != QLatin1StringView("updates")) {
142 qCCritical(AKONADISERVER_LOG) << "Invalid update description file format";
143 return false;
144 }
145
146 // iterate over the xml document and extract update information into an UpdateSet
147 QDomElement updateElement = documentElement.firstChildElement();
148 while (!updateElement.isNull()) {
149 if (updateElement.tagName() == QLatin1StringView("update")) {
150 const int version = updateElement.attribute(QStringLiteral("version"), QStringLiteral("-1")).toInt();
151 if (version <= 0) {
152 qCCritical(AKONADISERVER_LOG) << "Invalid version attribute in database update description";
153 return false;
154 }
155
156 if (updates.contains(version)) {
157 qCCritical(AKONADISERVER_LOG) << "Duplicate version attribute in database update description";
158 return false;
159 }
160
161 if (version <= currentVersion) {
162 qCDebug(AKONADISERVER_LOG) << "skipping update" << version;
163 } else {
164 UpdateSet updateSet;
165 updateSet.version = version;
166 updateSet.abortOnFailure = (updateElement.attribute(QStringLiteral("abortOnFailure")) == QLatin1StringView("true"));
167
168 QDomElement childElement = updateElement.firstChildElement();
169 while (!childElement.isNull()) {
170 if (childElement.tagName() == QLatin1StringView("raw-sql")) {
171 if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) {
172 updateSet.statements << buildRawSqlStatement(childElement);
173 }
174 } else if (childElement.tagName() == QLatin1StringView("complex-update")) {
175 if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) {
176 updateSet.complex = true;
177 }
178 }
179 // TODO: check for generic tags here in the future
180
181 childElement = childElement.nextSiblingElement();
182 }
183
184 if (!updateSet.statements.isEmpty() || updateSet.complex) {
185 updates.insert(version, updateSet);
186 }
187 }
188 }
189 updateElement = updateElement.nextSiblingElement();
190 }
191
192 return true;
193}
194
195bool DbUpdater::updateApplicable(const QString &backends) const
196{
197 const QStringList matchingBackends = backends.split(QLatin1Char(','));
198
199 QString currentBackend;
200 switch (DbType::type(m_database)) {
201 case DbType::MySQL:
202 currentBackend = QStringLiteral("mysql");
203 break;
204 case DbType::PostgreSQL:
205 currentBackend = QStringLiteral("psql");
206 break;
207 case DbType::Sqlite:
208 currentBackend = QStringLiteral("sqlite");
209 break;
210 case DbType::Unknown:
211 return false;
212 }
213
214 return matchingBackends.contains(currentBackend);
215}
216
217QString DbUpdater::buildRawSqlStatement(const QDomElement &element) const
218{
219 return element.text().trimmed();
220}
221
222bool DbUpdater::complexUpdate_25()
223{
224 qCDebug(AKONADISERVER_LOG) << "Starting database update to version 25";
225
226 DbType::Type dbType = DbType::type(m_database);
227 auto store = DataStore::dataStoreForDatabase(m_database);
228
229 QElapsedTimer ttotal;
230 ttotal.start();
231
232 // Recover from possibly failed or interrupted update
233 {
234 // We don't care if this fails, it just means that there was no failed update
235 QSqlQuery query(m_database);
236 query.exec(QStringLiteral("ALTER TABLE PartTable_old RENAME TO PartTable"));
237 }
238
239 {
240 QSqlQuery query(m_database);
241 query.exec(QStringLiteral("DROP TABLE IF EXISTS PartTable_new"));
242 }
243
244 {
245 // Make sure the table is empty, otherwise we get duplicate key error
246 QSqlQuery query(m_database);
247 if (dbType == DbType::Sqlite) {
248 query.exec(QStringLiteral("DELETE FROM PartTypeTable"));
249 } else { // MySQL, PostgreSQL
250 query.exec(QStringLiteral("TRUNCATE TABLE PartTypeTable"));
251 }
252 }
253
254 {
255 // It appears that more users than expected have the invalid "GID" part in their
256 // PartTable, which breaks the migration below (see BKO#331867), so we apply this
257 // wanna-be fix to remove the invalid part before we start the actual migration.
258 QueryBuilder qb(store, QStringLiteral("PartTable"), QueryBuilder::Delete);
259 qb.addValueCondition(QStringLiteral("PartTable.name"), Query::Equals, QLatin1StringView("GID"));
260 qb.exec();
261 }
262
263 qCDebug(AKONADISERVER_LOG) << "Creating a PartTable_new";
264 {
265 TableDescription description;
266 description.name = QStringLiteral("PartTable_new");
267
268 ColumnDescription idColumn;
269 idColumn.name = QStringLiteral("id");
270 idColumn.type = QStringLiteral("qint64");
271 idColumn.isAutoIncrement = true;
272 idColumn.isPrimaryKey = true;
273 description.columns << idColumn;
274
275 ColumnDescription pimItemIdColumn;
276 pimItemIdColumn.name = QStringLiteral("pimItemId");
277 pimItemIdColumn.type = QStringLiteral("qint64");
278 pimItemIdColumn.allowNull = false;
279 description.columns << pimItemIdColumn;
280
281 ColumnDescription partTypeIdColumn;
282 partTypeIdColumn.name = QStringLiteral("partTypeId");
283 partTypeIdColumn.type = QStringLiteral("qint64");
284 partTypeIdColumn.allowNull = false;
285 description.columns << partTypeIdColumn;
286
287 ColumnDescription dataColumn;
288 dataColumn.name = QStringLiteral("data");
289 dataColumn.type = QStringLiteral("QByteArray");
290 description.columns << dataColumn;
291
292 ColumnDescription dataSizeColumn;
293 dataSizeColumn.name = QStringLiteral("datasize");
294 dataSizeColumn.type = QStringLiteral("qint64");
295 dataSizeColumn.allowNull = false;
296 description.columns << dataSizeColumn;
297
298 ColumnDescription versionColumn;
299 versionColumn.name = QStringLiteral("version");
300 versionColumn.type = QStringLiteral("int");
301 versionColumn.defaultValue = QStringLiteral("0");
302 description.columns << versionColumn;
303
304 ColumnDescription externalColumn;
305 externalColumn.name = QStringLiteral("external");
306 externalColumn.type = QStringLiteral("bool");
307 externalColumn.defaultValue = QStringLiteral("false");
308 description.columns << externalColumn;
309
310 DbInitializer::Ptr initializer = DbInitializer::createInstance(m_database);
311 const QString queryString = initializer->buildCreateTableStatement(description);
312
313 QSqlQuery query(m_database);
314 if (!query.exec(queryString)) {
315 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
316 return false;
317 }
318 }
319
320 qCDebug(AKONADISERVER_LOG) << "Migrating part types";
321 {
322 // Get list of all part names
323 QueryBuilder qb(store, QStringLiteral("PartTable"), QueryBuilder::Select);
324 qb.setDistinct(true);
325 qb.addColumn(QStringLiteral("PartTable.name"));
326
327 if (!qb.exec()) {
328 qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text();
329 return false;
330 }
331
332 // Process them one by one
333 auto &query = qb.query();
334 while (query.next()) {
335 // Split the part name to namespace and name and insert it to PartTypeTable
336 const QString partName = query.value(0).toString();
337 const QString ns = partName.left(3);
338 const QString name = partName.mid(4);
339
340 {
341 QueryBuilder qb(store, QStringLiteral("PartTypeTable"), QueryBuilder::Insert);
342 qb.setColumnValue(QStringLiteral("ns"), ns);
343 qb.setColumnValue(QStringLiteral("name"), name);
344 if (!qb.exec()) {
345 qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text();
346 return false;
347 }
348 }
349 qCDebug(AKONADISERVER_LOG) << "\t Moved part type" << partName << "to PartTypeTable";
350 }
351 }
352
353 qCDebug(AKONADISERVER_LOG) << "Migrating data from PartTable to PartTable_new";
354 {
355 QSqlQuery query(m_database);
356 QString queryString;
357 if (dbType == DbType::PostgreSQL) {
358 queryString = QStringLiteral(
359 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
360 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
361 " PartTable.datasize, PartTable.version, PartTable.external "
362 "FROM PartTable "
363 "LEFT JOIN PartTypeTable ON "
364 " PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)");
365 } else if (dbType == DbType::MySQL) {
366 queryString = QStringLiteral(
367 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
368 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
369 "PartTable.datasize, PartTable.version, PartTable.external "
370 "FROM PartTable "
371 "LEFT JOIN PartTypeTable ON PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)");
372 } else if (dbType == DbType::Sqlite) {
373 queryString = QStringLiteral(
374 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
375 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
376 "PartTable.datasize, PartTable.version, PartTable.external "
377 "FROM PartTable "
378 "LEFT JOIN PartTypeTable ON PartTable.name = PartTypeTable.ns || ':' || PartTypeTable.name");
379 }
380
381 if (!query.exec(queryString)) {
382 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
383 return false;
384 }
385 }
386
387 qCDebug(AKONADISERVER_LOG) << "Swapping PartTable_new for PartTable";
388 {
389 // Does an atomic swap
390
391 QSqlQuery query(m_database);
392
393 if (dbType == DbType::PostgreSQL || dbType == DbType::Sqlite) {
394 if (dbType == DbType::PostgreSQL) {
395 m_database.transaction();
396 }
397
398 if (!query.exec(QStringLiteral("ALTER TABLE PartTable RENAME TO PartTable_old"))) {
399 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
400 m_database.rollback();
401 return false;
402 }
403
404 // If this fails in SQLite (i.e. without transaction), we can still recover on next start)
405 if (!query.exec(QStringLiteral("ALTER TABLE PartTable_new RENAME TO PartTable"))) {
406 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
407 m_database.rollback();
408 return false;
409 }
410
411 if (dbType == DbType::PostgreSQL) {
412 m_database.commit();
413 }
414 } else { // MySQL cannot do rename in transaction, but supports atomic renames
415 if (!query.exec(QStringLiteral("RENAME TABLE PartTable TO PartTable_old,"
416 " PartTable_new TO PartTable"))) {
417 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
418 return false;
419 }
420 }
421 }
422
423 qCDebug(AKONADISERVER_LOG) << "Removing PartTable_old";
424 {
425 QSqlQuery query(m_database);
426 if (!query.exec(QStringLiteral("DROP TABLE PartTable_old;"))) {
427 // It does not matter when this fails, we are successfully migrated
428 qCDebug(AKONADISERVER_LOG) << query.lastError().text();
429 qCDebug(AKONADISERVER_LOG) << "Not a fatal problem, continuing...";
430 }
431 }
432
433 // Fine tuning for PostgreSQL
434 qCDebug(AKONADISERVER_LOG) << "Final tuning of new PartTable";
435 {
436 QSqlQuery query(m_database);
437 if (dbType == DbType::PostgreSQL) {
438 query.exec(QStringLiteral("ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey"));
439 query.exec(QStringLiteral("ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq"));
440 query.exec(QStringLiteral("SELECT setval('parttable_id_seq', MAX(id) + 1) FROM PartTable"));
441 } else if (dbType == DbType::MySQL) {
442 // 0 will automatically reset AUTO_INCREMENT to SELECT MAX(id) + 1 FROM PartTable
443 query.exec(QStringLiteral("ALTER TABLE PartTable AUTO_INCREMENT = 0"));
444 }
445 }
446
447 qCDebug(AKONADISERVER_LOG) << "Update done in" << ttotal.elapsed() << "ms";
448
449 // Foreign keys and constraints will be reconstructed automatically once
450 // all updates are done
451
452 return true;
453}
454
455bool DbUpdater::complexUpdate_36()
456{
457 qCDebug(AKONADISERVER_LOG, "Starting database update to version 36");
458 Q_ASSERT(DbType::type(m_database) == DbType::Sqlite);
459
460 QSqlQuery query(m_database);
461 if (!query.exec(QStringLiteral("PRAGMA foreign_key_checks=OFF"))) {
462 qCCritical(AKONADISERVER_LOG, "Failed to disable foreign key checks!");
463 return false;
464 }
465
466 const auto hasForeignKeys = [](const TableDescription &desc) {
467 return std::any_of(desc.columns.cbegin(), desc.columns.cend(), [](const ColumnDescription &col) {
468 return !col.refTable.isEmpty() && !col.refColumn.isEmpty();
469 });
470 };
471
472 const auto recreateTableWithForeignKeys = [this](const TableDescription &table) -> QPair<bool, QSqlQuery> {
473 qCDebug(AKONADISERVER_LOG) << "Updating foreign keys in table" << table.name;
474
475 QSqlQuery query(m_database);
476
477 // Recover from possibly failed or interrupted update
478 // We don't care if this fails, it just means that there was no failed update
479 query.exec(QStringLiteral("ALTER TABLE %1_old RENAME TO %1").arg(table.name));
480 query.exec(QStringLiteral("DROP TABLE %1_new").arg(table.name));
481
482 qCDebug(AKONADISERVER_LOG, "\tCreating table %s_new with foreign keys", qUtf8Printable(table.name));
483 {
484 const auto initializer = DbInitializer::createInstance(m_database);
485 TableDescription copy = table;
486 copy.name += QStringLiteral("_new");
487 if (!query.exec(initializer->buildCreateTableStatement(copy))) {
488 // If this fails we will recover on next start
489 return {false, std::move(query)};
490 }
491 }
492
493 qCDebug(AKONADISERVER_LOG,
494 "\tCopying values from %s to %s_new (this may take a very long of time...)",
495 qUtf8Printable(table.name),
496 qUtf8Printable(table.name));
497 if (!query.exec(QStringLiteral("INSERT INTO %1_new SELECT * FROM %1").arg(table.name))) {
498 // If this fails, we will recover on next start
499 return {false, std::move(query)};
500 }
501
502 qCDebug(AKONADISERVER_LOG, "\tSwapping %s_new for %s", qUtf8Printable(table.name), qUtf8Printable(table.name));
503 if (!query.exec(QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name))) {
504 // If this fails we will recover on next start
505 return {false, std::move(query)};
506 }
507
508 if (!query.exec(QStringLiteral("ALTER TABLE %1_new RENAME TO %1").arg(table.name))) {
509 // If this fails we will recover on next start
510 return {false, std::move(query)};
511 }
512
513 qCDebug(AKONADISERVER_LOG, "\tRemoving table %s_old", qUtf8Printable(table.name));
514 if (!query.exec(QStringLiteral("DROP TABLE %1_old").arg(table.name))) {
515 // We don't care if this fails
516 qCWarning(AKONADISERVER_LOG, "Failed to DROP TABLE %s (not fatal, update will continue)", qUtf8Printable(table.name));
517 qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
518 }
519
520 qCDebug(AKONADISERVER_LOG) << "\tOptimizing table %s", qUtf8Printable(table.name);
521 if (!query.exec(QStringLiteral("ANALYZE %1").arg(table.name))) {
522 // We don't care if this fails
523 qCWarning(AKONADISERVER_LOG, "Failed to ANALYZE %s (not fatal, update will continue)", qUtf8Printable(table.name));
524 qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
525 }
526
527 qCDebug(AKONADISERVER_LOG) << "\tDone";
528 return {true, QSqlQuery()};
529 };
530
531 AkonadiSchema schema;
532 const auto tables = schema.tables();
533 for (const auto &table : tables) {
534 if (!hasForeignKeys(table)) {
535 continue;
536 }
537
538 const auto &[ok, query] = recreateTableWithForeignKeys(table);
539 if (!ok) {
540 qCCritical(AKONADISERVER_LOG, "SQL error when updating table %s", qUtf8Printable(table.name));
541 qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery()));
542 qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
543 return false;
544 }
545 }
546
547 const auto relations = schema.relations();
548 for (const auto &relation : relations) {
549 const RelationTableDescription table(relation);
550 const auto &[ok, query] = recreateTableWithForeignKeys(table);
551 if (!ok) {
552 qCCritical(AKONADISERVER_LOG, "SQL error when updating relation table %s", qUtf8Printable(table.name));
553 qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery()));
554 qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
555 return false;
556 }
557 }
558
559 qCDebug(AKONADISERVER_LOG) << "Running VACUUM to reduce DB size";
560 if (!query.exec(QStringLiteral("VACUUM"))) {
561 qCWarning(AKONADISERVER_LOG) << "Vacuum failed (not fatal, update will continue)";
562 qCWarning(AKONADISERVER_LOG) << "Error:" << query.lastError().text();
563 }
564
565 return true;
566}
567
568#include "moc_dbupdater.cpp"
A helper class that describes a column of a table for the DbInitializer.
Definition schematypes.h:23
static DataStore * dataStoreForDatabase(const QSqlDatabase &db)
Returns DataStore associated with the given database connection.
Definition datastore.cpp:98
static DbInitializer::Ptr createInstance(const QSqlDatabase &database, Schema *schema=nullptr)
Returns an initializer instance for a given backend.
DbUpdater(const QSqlDatabase &database, const QString &filename)
Creates a new database updates.
Definition dbupdater.cpp:37
bool run()
Starts the update process.
Definition dbupdater.cpp:43
Helper class to construct arbitrary SQL queries.
TableDescription constructed based on RelationDescription.
A helper class that describes a table for the DbInitializer.
Definition schematypes.h:77
A helper class that contains an update set.
Definition dbupdater.h:25
Type
Supported database types.
Definition dbtype.h:19
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)
KCOREADDONS_EXPORT unsigned int version()
QString name(StandardAction id)
const QList< QKeySequence > & copy()
const char * constData() const const
QDBusError lastError() const const
QDBusConnection sessionBus()
bool unregisterService(const QString &serviceName)
QString message() const const
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QString attribute(const QString &name, const QString &defValue) const const
QString tagName() const const
QString text() const const
QDomElement firstChildElement(const QString &tagName, const QString &namespaceURI) const const
bool isNull() const const
QDomElement nextSiblingElement(const QString &tagName, const QString &namespaceURI) const const
qint64 elapsed() const const
bool isEmpty() const const
T value(qsizetype i) const const
ConstIterator
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
bool invoke(QObject *obj, Args &&... arguments) const const
int indexOfMethod(const char *method) const const
QMetaMethod method(int index) const const
virtual const QMetaObject * metaObject() const const
QSqlError lastError() const const
bool rollback()
bool transaction()
QString text() const const
QString arg(Args &&... args) const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:01:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.