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
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 {
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
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
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
269 idColumn.name = QStringLiteral("id");
270 idColumn.type = QStringLiteral("qint64");
271 idColumn.isAutoIncrement = true;
272 idColumn.isPrimaryKey = true;
273 description.columns << idColumn;
274
276 pimItemIdColumn.name = QStringLiteral("pimItemId");
277 pimItemIdColumn.type = QStringLiteral("qint64");
278 pimItemIdColumn.allowNull = false;
279 description.columns << pimItemIdColumn;
280
282 partTypeIdColumn.name = QStringLiteral("partTypeId");
283 partTypeIdColumn.type = QStringLiteral("qint64");
284 partTypeIdColumn.allowNull = false;
285 description.columns << partTypeIdColumn;
286
288 dataColumn.name = QStringLiteral("data");
289 dataColumn.type = QStringLiteral("QByteArray");
290 description.columns << dataColumn;
291
293 dataSizeColumn.name = QStringLiteral("datasize");
294 dataSizeColumn.type = QStringLiteral("qint64");
295 dataSizeColumn.allowNull = false;
296 description.columns << dataSizeColumn;
297
299 versionColumn.name = QStringLiteral("version");
300 versionColumn.type = QStringLiteral("int");
301 versionColumn.defaultValue = QStringLiteral("0");
302 description.columns << versionColumn;
303
305 externalColumn.name = QStringLiteral("external");
306 externalColumn.type = QStringLiteral("bool");
307 externalColumn.defaultValue = QStringLiteral("false");
308 description.columns << externalColumn;
309
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 QSqlQuery 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 query.finish();
352 }
353
354 qCDebug(AKONADISERVER_LOG) << "Migrating data from PartTable to PartTable_new";
355 {
356 QSqlQuery query(m_database);
357 QString queryString;
358 if (dbType == DbType::PostgreSQL) {
359 queryString = QStringLiteral(
360 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
361 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
362 " PartTable.datasize, PartTable.version, PartTable.external "
363 "FROM PartTable "
364 "LEFT JOIN PartTypeTable ON "
365 " PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)");
366 } else if (dbType == DbType::MySQL) {
367 queryString = QStringLiteral(
368 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
369 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
370 "PartTable.datasize, PartTable.version, PartTable.external "
371 "FROM PartTable "
372 "LEFT JOIN PartTypeTable ON PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)");
373 } else if (dbType == DbType::Sqlite) {
374 queryString = QStringLiteral(
375 "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) "
376 "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, "
377 "PartTable.datasize, PartTable.version, PartTable.external "
378 "FROM PartTable "
379 "LEFT JOIN PartTypeTable ON PartTable.name = PartTypeTable.ns || ':' || PartTypeTable.name");
380 }
381
382 if (!query.exec(queryString)) {
383 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
384 return false;
385 }
386 }
387
388 qCDebug(AKONADISERVER_LOG) << "Swapping PartTable_new for PartTable";
389 {
390 // Does an atomic swap
391
392 QSqlQuery query(m_database);
393
394 if (dbType == DbType::PostgreSQL || dbType == DbType::Sqlite) {
395 if (dbType == DbType::PostgreSQL) {
396 m_database.transaction();
397 }
398
399 if (!query.exec(QStringLiteral("ALTER TABLE PartTable RENAME TO PartTable_old"))) {
400 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
401 m_database.rollback();
402 return false;
403 }
404
405 // If this fails in SQLite (i.e. without transaction), we can still recover on next start)
406 if (!query.exec(QStringLiteral("ALTER TABLE PartTable_new RENAME TO PartTable"))) {
407 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
408 m_database.rollback();
409 return false;
410 }
411
412 if (dbType == DbType::PostgreSQL) {
413 m_database.commit();
414 }
415 } else { // MySQL cannot do rename in transaction, but supports atomic renames
416 if (!query.exec(QStringLiteral("RENAME TABLE PartTable TO PartTable_old,"
417 " PartTable_new TO PartTable"))) {
418 qCCritical(AKONADISERVER_LOG) << query.lastError().text();
419 return false;
420 }
421 }
422 }
423
424 qCDebug(AKONADISERVER_LOG) << "Removing PartTable_old";
425 {
426 QSqlQuery query(m_database);
427 if (!query.exec(QStringLiteral("DROP TABLE PartTable_old;"))) {
428 // It does not matter when this fails, we are successfully migrated
429 qCDebug(AKONADISERVER_LOG) << query.lastError().text();
430 qCDebug(AKONADISERVER_LOG) << "Not a fatal problem, continuing...";
431 }
432 }
433
434 // Fine tuning for PostgreSQL
435 qCDebug(AKONADISERVER_LOG) << "Final tuning of new PartTable";
436 {
437 QSqlQuery query(m_database);
438 if (dbType == DbType::PostgreSQL) {
439 query.exec(QStringLiteral("ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey"));
440 query.exec(QStringLiteral("ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq"));
441 query.exec(QStringLiteral("SELECT setval('parttable_id_seq', MAX(id) + 1) FROM PartTable"));
442 } else if (dbType == DbType::MySQL) {
443 // 0 will automatically reset AUTO_INCREMENT to SELECT MAX(id) + 1 FROM PartTable
444 query.exec(QStringLiteral("ALTER TABLE PartTable AUTO_INCREMENT = 0"));
445 }
446 }
447
448 qCDebug(AKONADISERVER_LOG) << "Update done in" << ttotal.elapsed() << "ms";
449
450 // Foreign keys and constraints will be reconstructed automatically once
451 // all updates are done
452
453 return true;
454}
455
456bool DbUpdater::complexUpdate_36()
457{
458 qCDebug(AKONADISERVER_LOG, "Starting database update to version 36");
459 Q_ASSERT(DbType::type(m_database) == DbType::Sqlite);
460
461 QSqlQuery query(m_database);
462 if (!query.exec(QStringLiteral("PRAGMA foreign_key_checks=OFF"))) {
463 qCCritical(AKONADISERVER_LOG, "Failed to disable foreign key checks!");
464 return false;
465 }
466
467 const auto hasForeignKeys = [](const TableDescription &desc) {
468 return std::any_of(desc.columns.cbegin(), desc.columns.cend(), [](const ColumnDescription &col) {
469 return !col.refTable.isEmpty() && !col.refColumn.isEmpty();
470 });
471 };
472
473 const auto recreateTableWithForeignKeys = [this](const TableDescription &table) -> QPair<bool, QSqlQuery> {
474 qCDebug(AKONADISERVER_LOG) << "Updating foreign keys in table" << table.name;
475
476 QSqlQuery query(m_database);
477
478 // Recover from possibly failed or interrupted update
479 // We don't care if this fails, it just means that there was no failed update
480 query.exec(QStringLiteral("ALTER TABLE %1_old RENAME TO %1").arg(table.name));
481 query.exec(QStringLiteral("DROP TABLE %1_new").arg(table.name));
482
483 qCDebug(AKONADISERVER_LOG, "\tCreating table %s_new with foreign keys", qUtf8Printable(table.name));
484 {
485 const auto initializer = DbInitializer::createInstance(m_database);
486 TableDescription copy = table;
487 copy.name += QStringLiteral("_new");
488 if (!query.exec(initializer->buildCreateTableStatement(copy))) {
489 // If this fails we will recover on next start
490 return {false, query};
491 }
492 }
493
494 qCDebug(AKONADISERVER_LOG,
495 "\tCopying values from %s to %s_new (this may take a very long of time...)",
496 qUtf8Printable(table.name),
497 qUtf8Printable(table.name));
498 if (!query.exec(QStringLiteral("INSERT INTO %1_new SELECT * FROM %1").arg(table.name))) {
499 // If this fails, we will recover on next start
500 return {false, query};
501 }
502
503 qCDebug(AKONADISERVER_LOG, "\tSwapping %s_new for %s", qUtf8Printable(table.name), qUtf8Printable(table.name));
504 if (!query.exec(QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name))) {
505 // If this fails we will recover on next start
506 return {false, query};
507 }
508
509 if (!query.exec(QStringLiteral("ALTER TABLE %1_new RENAME TO %1").arg(table.name))) {
510 // If this fails we will recover on next start
511 return {false, query};
512 }
513
514 qCDebug(AKONADISERVER_LOG, "\tRemoving table %s_old", qUtf8Printable(table.name));
515 if (!query.exec(QStringLiteral("DROP TABLE %1_old").arg(table.name))) {
516 // We don't care if this fails
517 qCWarning(AKONADISERVER_LOG, "Failed to DROP TABLE %s (not fatal, update will continue)", qUtf8Printable(table.name));
518 qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
519 }
520
521 qCDebug(AKONADISERVER_LOG) << "\tOptimizing table %s", qUtf8Printable(table.name);
522 if (!query.exec(QStringLiteral("ANALYZE %1").arg(table.name))) {
523 // We don't care if this fails
524 qCWarning(AKONADISERVER_LOG, "Failed to ANALYZE %s (not fatal, update will continue)", qUtf8Printable(table.name));
525 qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
526 }
527
528 qCDebug(AKONADISERVER_LOG) << "\tDone";
529 return {true, QSqlQuery()};
530 };
531
532 AkonadiSchema schema;
533 const auto tables = schema.tables();
534 for (const auto &table : tables) {
535 if (!hasForeignKeys(table)) {
536 continue;
537 }
538
539 const auto &[ok, query] = recreateTableWithForeignKeys(table);
540 if (!ok) {
541 qCCritical(AKONADISERVER_LOG, "SQL error when updating table %s", qUtf8Printable(table.name));
542 qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery()));
543 qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
544 return false;
545 }
546 }
547
548 const auto relations = schema.relations();
549 for (const auto &relation : relations) {
550 const RelationTableDescription table(relation);
551 const auto &[ok, query] = recreateTableWithForeignKeys(table);
552 if (!ok) {
553 qCCritical(AKONADISERVER_LOG, "SQL error when updating relation table %s", qUtf8Printable(table.name));
554 qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery()));
555 qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text()));
556 return false;
557 }
558 }
559
560 qCDebug(AKONADISERVER_LOG) << "Running VACUUM to reduce DB size";
561 if (!query.exec(QStringLiteral("VACUUM"))) {
562 qCWarning(AKONADISERVER_LOG) << "Vacuum failed (not fatal, update will continue)";
563 qCWarning(AKONADISERVER_LOG) << "Error:" << query.lastError().text();
564 }
565
566 return true;
567}
568
569#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:94
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)
KDB_EXPORT KDbVersionInfo version()
QString name(StandardShortcut 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 tagName() const const
QString text() const const
QDomElement firstChildElement(const QString &tagName, const QString &namespaceURI) const const
T value(qsizetype i) 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
T qobject_cast(QObject *object)
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
QByteArray toLatin1() const const
QString trimmed() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.